diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml index fb15beb726..a975c0b995 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml @@ -40,7 +40,7 @@ body: label: Database description: What database are you using? (self-hosters only) options: - - CockroachDB + - CockroachDB (Zitadel v2) - PostgreSQL - Other (describe below!) - type: input diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8a2f567304..b7354f3f4a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,17 @@ version: 2 updates: - package-ecosystem: npm - directory: "/console" + groups: + console: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" + directories: + - "/console" + - "/e2e" schedule: interval: weekly time: "02:00" @@ -13,6 +23,14 @@ updates: prefix: chore include: scope - package-ecosystem: gomod + groups: + go: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" directory: "/" schedule: interval: weekly @@ -25,6 +43,14 @@ updates: prefix: chore include: scope - package-ecosystem: "docker" + groups: + docker: + applies-to: version-updates + patterns: + - "*" + update-types: + - "minor" + - "patch" directory: "/build" schedule: interval: "weekly" @@ -34,6 +60,11 @@ updates: prefix: chore include: scope - package-ecosystem: "github-actions" + groups: + actions: + applies-to: version-updates + patterns: + - "*" directory: "/" schedule: interval: weekly diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 265902feff..979911d5ab 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,8 @@ + + # Which Problems Are Solved Replace this example text with a concise list of problems that this PR solves. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ab5e6bafd..c5e99b95b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,6 @@ jobs: with: node_version: "20" buf_version: "latest" - go_version: "1.23" console: uses: ./.github/workflows/console.yml @@ -43,7 +42,6 @@ jobs: needs: [core, console, version] uses: ./.github/workflows/compile.yml with: - go_version: "1.23" core_cache_key: ${{ needs.core.outputs.cache_key }} console_cache_key: ${{ needs.console.outputs.cache_key }} core_cache_path: ${{ needs.core.outputs.cache_path }} @@ -54,7 +52,6 @@ jobs: needs: core uses: ./.github/workflows/core-unit-test.yml with: - go_version: "1.23" core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_path: ${{ needs.core.outputs.cache_path }} secrets: @@ -64,7 +61,6 @@ jobs: needs: core uses: ./.github/workflows/core-integration-test.yml with: - go_version: "1.23" core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_path: ${{ needs.core.outputs.cache_path }} secrets: @@ -74,7 +70,6 @@ jobs: needs: [core, console] uses: ./.github/workflows/lint.yml with: - go_version: "1.23" node_version: "18" buf_version: "latest" go_lint_version: "v1.62.2" diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 979f8b4bc1..519586b9ee 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -3,9 +3,6 @@ name: Compile on: workflow_call: inputs: - go_version: - required: true - type: string core_cache_key: required: true type: string @@ -53,7 +50,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go_version }} + go-version-file: 'go.mod' - name: compile timeout-minutes: 5 diff --git a/.github/workflows/console.yml b/.github/workflows/console.yml index 38e75a069b..3e77757129 100644 --- a/.github/workflows/console.yml +++ b/.github/workflows/console.yml @@ -23,8 +23,7 @@ jobs: outputs: cache_key: ${{ steps.cache.outputs.cache-primary-key }} cache_path: ${{ env.cache_path }} - runs-on: - group: zitadel-public + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 5e22a67413..33ffd4f6af 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -53,8 +53,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - with: - driver-opts: 'image=moby/buildkit:v0.11.6' - name: Login to Docker registry uses: docker/login-action@v3 @@ -75,7 +73,7 @@ jobs: - name: Debug id: build-debug - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 timeout-minutes: 3 with: context: . @@ -90,7 +88,7 @@ jobs: - name: Scratch id: build-scratch - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 timeout-minutes: 3 with: context: . @@ -147,8 +145,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - with: - driver-opts: 'image=moby/buildkit:v0.11.6' - name: Login to Docker registry uses: docker/login-action@v3 diff --git a/.github/workflows/core-integration-test.yml b/.github/workflows/core-integration-test.yml index cc9d898f5c..e33ffec3d5 100644 --- a/.github/workflows/core-integration-test.yml +++ b/.github/workflows/core-integration-test.yml @@ -3,9 +3,6 @@ name: Integration test core on: workflow_call: inputs: - go_version: - required: true - type: string core_cache_key: required: true type: string @@ -46,7 +43,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go_version }} + go-version-file: 'go.mod' - uses: actions/cache/restore@v4 timeout-minutes: 1 @@ -76,7 +73,6 @@ jobs: if: ${{ steps.cache.outputs.cache-hit != 'true' }} env: ZITADEL_MASTERKEY: MasterkeyNeedsToHave32Characters - INTEGRATION_DB_FLAVOR: postgres run: make core_integration_test - name: upload server logs @@ -102,71 +98,3 @@ jobs: with: key: integration-test-postgres-${{ inputs.core_cache_key }} path: ${{ steps.go-cache-path.outputs.GO_CACHE_PATH }} - - # TODO: produces the following output: ERROR: unknown command "cockroach start-single-node --insecure" for "cockroach" - # cockroach: - # runs-on: ubuntu-latest - # services: - # cockroach: - # image: cockroachdb/cockroach:latest - # ports: - # - 26257:26257 - # - 8080:8080 - # env: - # COCKROACH_ARGS: "start-single-node --insecure" - # options: >- - # --health-cmd "curl http://localhost:8080/health?ready=1 || exit 1" - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # --health-start-period 10s - # steps: - # - - # uses: actions/checkout@v4 - # - - # uses: actions/setup-go@v5 - # with: - # go-version: ${{ inputs.go_version }} - # - - # uses: actions/cache/restore@v4 - # timeout-minutes: 1 - # name: restore core - # with: - # path: ${{ inputs.core_cache_path }} - # key: ${{ inputs.core_cache_key }} - # fail-on-cache-miss: true - # - - # id: go-cache-path - # name: set cache path - # run: echo "GO_CACHE_PATH=$(go env GOCACHE)" >> $GITHUB_OUTPUT - # - - # uses: actions/cache/restore@v4 - # id: cache - # timeout-minutes: 1 - # name: restore previous results - # with: - # key: integration-test-crdb-${{ inputs.core_cache_key }} - # restore-keys: | - # integration-test-crdb-core- - # path: ${{ steps.go-cache-path.outputs.GO_CACHE_PATH }} - # - - # name: test - # if: ${{ steps.cache.outputs.cache-hit != 'true' }} - # env: - # ZITADEL_MASTERKEY: MasterkeyNeedsToHave32Characters - # INTEGRATION_DB_FLAVOR: cockroach - # run: make core_integration_test - # - - # name: publish coverage - # uses: codecov/codecov-action@v4.3.0 - # with: - # file: profile.cov - # name: core-integration-tests-cockroach - # flags: core-integration-tests-cockroach - # - - # uses: actions/cache/save@v4 - # name: cache results - # if: ${{ steps.cache.outputs.cache-hit != 'true' }} - # with: - # key: integration-test-crdb-${{ inputs.core_cache_key }} - # path: ${{ steps.go-cache-path.outputs.GO_CACHE_PATH }} \ No newline at end of file diff --git a/.github/workflows/core-unit-test.yml b/.github/workflows/core-unit-test.yml index 0b1467ff5d..715a08eb19 100644 --- a/.github/workflows/core-unit-test.yml +++ b/.github/workflows/core-unit-test.yml @@ -3,9 +3,6 @@ name: Unit test core on: workflow_call: inputs: - go_version: - required: true - type: string core_cache_key: required: true type: string @@ -21,15 +18,14 @@ on: jobs: test: - runs-on: - group: zitadel-public + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go_version }} + go-version-file: 'go.mod' - uses: actions/cache/restore@v4 timeout-minutes: 1 diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 79aa6ddd1d..13e7c0dee7 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -3,9 +3,6 @@ name: Build core on: workflow_call: inputs: - go_version: - required: true - type: string buf_version: required: true type: string @@ -31,8 +28,7 @@ env: jobs: build: - runs-on: - group: zitadel-public + runs-on: ubuntu-latest outputs: cache_key: ${{ steps.cache.outputs.cache-primary-key }} cache_path: ${{ env.cache_path }} @@ -70,7 +66,7 @@ jobs: if: ${{ steps.cache.outputs.cache-hit != 'true' }} uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go_version }} + go-version-file: 'go.mod' - if: ${{ steps.cache.outputs.cache-hit != 'true' }} run: make core_build diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4100347d6d..e717163507 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -10,8 +10,7 @@ jobs: fail-fast: false matrix: browser: [firefox, chrome] - runs-on: - group: zitadel-public + runs-on: ubuntu-latest steps: - name: Checkout Repository @@ -32,8 +31,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - with: - driver-opts: 'image=moby/buildkit:v0.11.6' - name: Start DB and ZITADEL run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0aa3b81737..e704bdb146 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,9 +6,6 @@ on: node_version: required: true type: string - go_version: - required: true - type: string buf_version: required: true type: string @@ -85,7 +82,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go_version }} + go-version-file: 'go.mod' - uses: actions/cache/restore@v4 timeout-minutes: 1 diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index cf11e944f8..063f6956a5 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -32,6 +32,7 @@ jobs: if: ${{ !inputs.dry_run }} with: path: .artifacts + pattern: "{checksums.txt,zitadel-*}" - name: Semantic Release uses: cycjimmy/semantic-release-action@v4 diff --git a/.gitignore b/.gitignore index 17aee6bbe9..23469d4209 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,6 @@ load-test/.keys # dumps .backups -cockroach-data/* .local/* .build/ @@ -71,7 +70,6 @@ zitadel-*-* # local build/local/*.env -migrations/cockroach/migrate_cloud.go .notifications /.artifacts/* !/.artifacts/zitadel diff --git a/.golangci.yaml b/.golangci.yaml index f480eb8c10..1cae359605 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -4,12 +4,7 @@ issues: max-issues-per-linter: 0 # Set to 0 to disable. max-same-issues: 0 - -run: - concurrency: 4 - timeout: 10m - go: '1.22' - skip-dirs: + exclude-dirs: - .artifacts - .backups - .codecov @@ -25,6 +20,11 @@ run: - openapi - proto - tools + +run: + concurrency: 4 + timeout: 10m + go: '1.22' linters: enable: # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] diff --git a/ADOPTERS.md b/ADOPTERS.md index 0573099cf9..876d984347 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -23,6 +23,7 @@ If you are using Zitadel, please consider adding yourself as a user with a quick | OpenAIP | [@openaip](https://github.com/openAIP) | Using Zitadel Cloud for everything related to user authentication. | | Smat.io | [@smatio](https://github.com/smatio) - [@lukasver](https://github.com/lukasver) | Zitadel for authentication in cloud applications while offering B2B portfolio management solutions for professional investors | | roclub GmbH | [@holgerson97](https://github.com/holgerson97) | Roclub builds a telehealth application to enable remote MRI/CT examinations. | +| CEEX AG | [@cleanenergyexchange](https://github.com/cleanenergyexchange) | Using Zitadel cloud for our SaaS products that support the sustainabel energy transistion | | Organization Name | contact@example.com | Description of how they use Zitadel | | Individual Name | contact@example.com | Description of how they use Zitadel | diff --git a/API_DESIGN.md b/API_DESIGN.md new file mode 100644 index 0000000000..ea37df5a24 --- /dev/null +++ b/API_DESIGN.md @@ -0,0 +1,378 @@ +# API Design + +This document describes the design principles and conventions for the ZITADEL API. It is scoped to the services and +endpoints of the proprietary ZITADEL API and does not cover any standardized APIs like OAuth 2, OpenID Connect or SCIM. + +## The Basics +ZITADEL follows an API first approach. This means all features can not only be accessed via the UI but also via the API. +The API is designed using the Protobuf specification. The Protobuf specification is then used to generate the API client +and server code in different programming languages. +The API is designed to be used by different clients, such as web applications, mobile applications, and other services. +Therefore, the API is designed to be easy to use, consistent, and reliable. + +Starting with the V2 API, the API and its services use a resource-oriented design. +This means that the API is designed around resources, which are the key entities in the system. +Each resource has a unique identifier and a set of properties that describe the resource. +The entire lifecycle of a resource can be managed using the API. + +> [!IMPORTANT] +> This style guide is a work in progress and will be updated over time. +> Not all parts of the API might follow the guidelines yet. +> However, all new endpoints and services MUST be designed according to this style guide. + +### Protobuf, gRPC and connectRPC + +The API is designed using the Protobuf specification. The Protobuf specification is used to define the API services, messages, and methods. +Starting with the V2 API, the API uses connectRPC as the main transport protocol. +[connectRPC](https://connectrpc.com/) is a protocol that is based on gRPC and HTTP/2. +It allows clients to call the API using connectRPC, gRPC and also HTTP/1.1. + +## Conventions + +The API follows the base conventions of Protobuf and connectRPC. + +Please check out their style guides and concepts for more information: +- Protobuf: https://protobuf.dev/programming-guides/style/ +- gRPC: https://grpc.io/docs/what-is-grpc/core-concepts/ +- Buf: https://buf.build/docs/best-practices/style-guide/ + +Additionally, there are some conventions that are specific to the ZITADEL API. +These conventions are described in the following sections. + +### Versioning + +The services and messages are versioned using major version numbers. This means that any change within a major version number is backward compatible. +Any breaking change requires a new major version number. +Each service is versioned independently. This means that a service can have a different version number than another service. +When creating a new service, start with version `2`, as version `1` is reserved for the old context based API and services. + +Please check out the structure Buf style guide for more information about the folder and package structure: https://buf.build/docs/best-practices/style-guide/ + +### Explicitness + +Make the handling of the API as explicit as possible. Do not make assumptions about the client's knowledge of the system or the API. +Provide clear and concise documentation for the API. + +Do not rely on implicit fallbacks or defaults if the client does not provide certain parameters. +Only use defaults if they are explicitly documented, such as returning a result set for the whole instance if no filter is provided. + +Some API calls may create multiple resources such as in the case of `zitadel.org.v2.AddOrganization`, where you can create an organization AND multiple users as admin. +In such cases the response should include **ALL** created resources and their ids. This removes any ambiguity from the users perspective whether or not +the additional resources were created and it also helps in testing. + +### Naming Conventions + +Names of resources, fields and methods MUST be descriptive and consistent. +Use domain-specific terminology and avoid abbreviations. +For example, use `organization_id` instead of **org_id** or **resource_owner** for the creation of a new user or when returning one. + +> [!NOTE] +> We'll update the resources in the [concepts section](https://zitadel.com/docs/concepts/structure/instance) to describe +> common resources and their meaning. +> Until then, please refer to the following issue: https://github.com/zitadel/zitadel/issues/5888 + +#### Resources and Fields + +When a context is required for creating a resource, the context is added as a field to the resource. +For example, when creating a new user, the organization's id is required. The `organization_id` is added as a field to the `CreateUserRequest`. + +```protobuf +message CreateUserRequest { + ... + string organization_id = 7 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + ]; + ... +} +``` + +Only allow providing a context where it is required. The context MUST not be provided if not required. +For example, when retrieving or updating a user, the `organization_id` is not required, since the user can be determined by the user's id. +However, it is possible to provide the `organization_id` as a filter to retrieve a list of users of a specific organization. + +Prevent the creation of global messages that are used in multiple resources unless they always follow the same pattern. +Use dedicated fields as described above or create a separate message for the specific context, that is only used in the boundary of the same resource. +For example, settings might be set as a default on the instance level, but might be overridden on the organization level. +In this case, the settings could share the same `SettingsContext` message to determine the context of the settings. +But do not create a global `Context` message that is used across the whole API if there are different scenarios and different fields required for the context. +The same applies to messages that are returned by multiple resources. +For example, information about the `User` might be different when managing the user resource itself than when it's returned +as part of an authorization or a manager role, where only limited information is needed. + +Prevent reusing messages for the creation and the retrieval of a resource. +Returning messages might contain additional information that is not required or even not available for the creation of the resource. +What might sound obvious when designing the CreateUserRequest for example, where only an `organization_id` but not the +`organization_name` is available, might not be so obvious when designing some sub-resource like a user's `IdentityProviderLink`, +which might contain an `identity_provider_name` when returned but not when created. + +```protobuf +message CreateUserRequest { + ... + repreated AddIdentityProviderLink identity_provider_links = 8; + ... +} + +message AddIdentityProviderLink { + string identity_provider_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + ]; + string user_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + ]; + string user_name = 3; +} + +message IdentiyProviderLink { + string identity_provider_id = 1; + string identity_provider_name = 2; + string user_id = 3; + string user_name = 4; +} +``` + +#### Operations and Methods + +Methods on a resource MUST be named using the following convention: + +| Operation | Method Name | Description | +|-----------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Create | Create\ | Create a new resource. If the new resource conflicts with an existing resources uniqueness (id, loginname, ...) the creation MUST be prevented and an error returned. | +| Update | Update\ | Update an existing resource. In most cases this SHOULD allow partial updates. If there are exception, they MUST be explicitly documented on the endpoint. The resource MUST already exists. An error is returned otherwise. | +| Delete | Delete\ | Delete an existing resource. If the resource does not exist, no error SHOULD be returned. In case of an exception to this rule, the behavior MUST clearly be documented. | +| Set | Set\ | Set a resource. This will replace the existing resource with the new resource. In case where the creation and update of a resource do not need to be differentiated, a single `Set` method SHOULD be used. It SHOULD allow partial changes. | +| Get | Get\ | Retrieve a single resource by its unique identifier. If the resource does not exist, an error MUST be returned. | +| List | List\ | Retrieve a list of resources. The endpoint SHOULD provide options to filter, sort and paginate. | + +Methods on a list of resources MUST be named using the following convention: + +| Operation | Method Name | Description | +|-----------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Add | Add\ | Add a new resource to a list. Any existing unique constraint (id, loginname, ...) will prevent the addition and return an error. | +| Remove | Remove\ | Remove an existing resource from a list. If the resource does not exist in the list, no error SHOULD be returned. In case of an exception to this rule, the behavior MUST clearly be documented. | +| Set | Set\ | Set a list of resources. This will replace the existing list with the new list. | + +Additionally, state changes, specific actions or operations that do not fit into the CRUD operations SHOULD be named according to the action that is performed: +- `Activate` or `Deactivate` for enabling or disabling a resource. +- `Verify` for verifying a resource. +- `Send` for sending a resource. +- etc. + +## Authentication and Authorization + +The API uses OAuth 2 for authorization. There are corresponding middlewares that check the access token for validity and +automatically return an error if the token is invalid. + +Permissions grated to the user might be organization specific and can therefore only be checked based on the queried resource. +In such case, the API does not check the permissions itself but relies on the checks of the functions that are called by the API. +If the permission can be checked by the API itself, e.g. if the permission is instance wide, it can be annotated on the endpoint in the proto file (see below). +In any case, the required permissions need to be documented in the [API documentation](#documentation). + +### Permission annotations + +Permissions can be annotated on the endpoint in the proto file. This allows the API to automatically check the permissions for the user. +The permissions are checked by the middleware and an error is returned if the user does not have the required permissions. + +The following example requires the user to have the `iam.web_key.write` permission to call the `CreateWebKey` method. +```protobuf + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } +}; +``` + +In case the permission cannot be checked by the API itself, but all requests need to be from an authenticated user, the `auth_option` can be set to `authenticated`. +```protobuf + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } +}; +``` + +## Pagination + +The API uses pagination for listing resources. The client can specify a limit and an offset to retrieve a subset of the resources. +Additionally, the client can specify sorting options to sort the resources by a specific field. + +Most listing methods SHOULD provide use the `ListQuery` message to allow the client to specify the limit, offset, and sorting options. +```protobuf + +// ListQuery is a general query object for lists to allow pagination and sorting. +message ListQuery { + uint64 offset = 1; + // limit is the maximum amount of objects returned. The default is set to 100 + // with a maximum of 1000 in the runtime configuration. + // If the limit exceeds the maximum configured ZITADEL will throw an error. + // If no limit is present the default is taken. + uint32 limit = 2; + // Asc is the sorting order. If true the list is sorted ascending, if false + // the list is sorted descending. The default is descending. + bool asc = 3; +} +``` +On the corresponding responses the `ListDetails` can be used to return the total count of the resources +and allow the user to handle their offset and limit accordingly. + +The API MUST enforce a reasonable maximum limit for the number of resources that can be retrieved and returned in a single request. +The default limit is set to 100 and the maximum limit is set to 1000. If the client requests a limit that exceeds the maximum limit, an error is returned. + +## Error Handling + +The API returns machine-readable errors in the response body. This includes a status code, an error code and possibly +some details about the error. See the following sections for more information about the status codes, error codes and error messages. + +### Status Codes + +The API uses status codes to indicate the status of a request. Depending on the protocol used to call the API, +the status code is returned as an HTTP status code or as a gRPC / connectRPC status code. +Check the possible status codes https://zitadel.com/docs/apis/statuscodes + +### Error Codes + +Additionally to the status code, the API returns unique error codes for each type of error. +The error codes are used to identify a specific error and can be used to handle the error programmatically. + +> [!NOTE] +> Currently, ZITADEL might already return some error codes. However, they do not follow a specific pattern yet +> and are not documented. We will update the error codes and document them in the future. + +### Error Message and Details + +The API returns additional details about the error in the response body. +This includes a human-readable error message and additional information that can help the client to understand the error +as well as machine-readable details that can be used to handle the error programmatically. +Error details use the Google RPC error details format: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto + +### Example + +HTTP/1.1 example: +``` +HTTP/1.1 400 Bad Request +Content-Type: application/json + +{ + "code": "user_invalid_information", + "message": "invalid or missing information provided for the creation of the user", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.BadRequest", + "fieldViolations": [ + { + "field": "given_name", + "description": "given name is required", + "reason": "MISSING_VALUE" + }, + { + "field": "family_name", + "description": "family name must not exceed 200 characters", + "reason": "INVALID_LENGTH" + } + ] + } + ] +} +``` + +gRPC / connectRPC example: +``` +HTTP/2.0 200 OK +Content-Type: application/grpc +Grpc-Message: invalid information provided for the creation of the user +Grpc-Status: 3 + +{ + "code": "user_invalid_information", + "message": "invalid or missing information provided for the creation of the user", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.BadRequest", + "fieldViolations": [ + { + "field": "given_name", + "description": "given name is required", + "reason": "MISSING_VALUE" + }, + { + "field": "family_name", + "description": "family name must not exceed 200 characters", + "reason": "INVALID_LENGTH" + } + ] + } + ] +} +``` + +### Documentation + +- Document the purpose of the API, the services, the endpoints, the request and response messages, the error codes and the status codes. +- Describe the fields of the request and response messages, the purpose and if needed the constraints. +- Document if the endpoints requires specific permissions or roles. +- Document and explain the possible error codes and the error messages that can be returned by the API. + +#### Examples + +```protobuf +// CreateUser will create a new user (human or machine) in the specified organization. +// The username must be unique. +// +// For human users: +// The user will receive a verification email if the email address is not marked as verified. +// You can pass a hashed_password. This allows migrating your users from your own system to ZITADEL, without any password +// reset for the user. Please check the required format and supported algorithms: . +// +// Required permission: +// - user.write +// +// Error Codes: +// - user_missing_information: The request is missing required information (either given_name, family_name and/or email) or contains invalid data for the creation of the user. Check error details for the missing or invalid fields. +// - user_already_exists: The user already exists. The username must be unique. +// - invalid_request: Your request does not have a valid format. Check error details for the reason. +// - permission_denied: You do not have the required permissions to access the requested resource. +// - unauthenticated: You are not authenticated. Please provide a valid access token. +rpc CreatUser(CreatUserRequest) returns (CreatUserResponse) {} +``` + +```protobuf +// ListUsers will return all matching users. By default, we will return all users of your instance that you have permission to read. Make sure to include a limit and sorting for pagination. +// +// Required permission: +// - user.read +// - no permission required to own user +// +// Error Codes: +// - invalid_request: Your request does not have a valid format. Check error details for the reason. +// - permission_denied: You do not have the required permissions to access the requested resource. +// - unauthenticated: You are not authenticated. Please provide a valid access token. +rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {} +``` + +```protobuf +// VerifyEmail will verify the provided verification code and mark the email as verified on success. +// An error is returned if the verification code is invalid or expired or if the user does not exist. +// Note that if multiple verification codes are generated, only the last one is valid. +// +// Required permission: +// - no permission required, the user must be authenticated +// +// Error Codes: +// - invalid_verification_code: The verification code is invalid or expired. +// - invalid_request: Your request does not have a valid format. Check error details for the reason. +// - unauthenticated: You are not authenticated. Please provide a valid access token. +rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse) {} +``` + +```protobuf +message VerifyEmailRequest{ + // The id of the user to verify the email for. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200} + ]; + // The verification code generated and sent to the user. + string verification_code = 2 [ + (validate.rules).string = {min_len: 1, max_len: 20} + ]; +} + +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e56ca307d1..ce8b9aff89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,6 +141,13 @@ Replace "policeman" with "police officer," "manpower" with "workforce," and "bus Ableist language includes words or phrases such as crazy, insane, blind to or blind eye to, cripple, dumb, and others. Choose alternative words depending on the context. +### API + +ZITADEL follows an API first approach. This means all features can not only be accessed via the UI but also via the API. +The API is designed to be used by different clients, such as web applications, mobile applications, and other services. +Therefore, the API is designed to be easy to use, consistent, and reliable. +Please check out the dedicated [API guidelines](./API_DESIGN.md) page when contributing to the API. + ### Developing ZITADEL with Dev Containers Follow the instructions provided by your code editor/IDE to initiate the development container. This typically involves opening the "Command Palette" or similar functionality and searching for commands related to "Dev Containers" or "Remote Containers". The quick start guide for VS Code can found [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container) @@ -158,7 +165,7 @@ ZITADEL serves traffic as soon as you can see the following log line: ### Backend/login By executing the commands from this section, you run everything you need to develop the ZITADEL backend locally. -Using [Docker Compose](https://docs.docker.com/compose/), you run a [CockroachDB](https://www.cockroachlabs.com/docs/stable/start-a-local-cluster-in-docker-mac.html) on your local machine. +Using [Docker Compose](https://docs.docker.com/compose/), you run a [PostgreSQL](https://www.postgresql.org/download/) on your local machine. With [make](https://www.gnu.org/software/make/), you build a debuggable ZITADEL binary and run it using [delve](https://github.com/go-delve/delve). Then, you test your changes via the console your binary is serving at http://localhost:8080 and by verifying the database. Once you are happy with your changes, you run end-to-end tests and tear everything down. @@ -193,7 +200,7 @@ make compile You can now run and debug the binary in .artifacts/zitadel/zitadel using your favourite IDE, for example GoLand. You can test if ZITADEL does what you expect by using the UI at http://localhost:8080/ui/console. -Also, you can verify the data by running `cockroach sql --database zitadel --insecure` and running SQL queries. +Also, you can verify the data by running `psql "host=localhost dbname=zitadel sslmode=disable"` and running SQL queries. #### Run Local Unit Tests @@ -209,12 +216,6 @@ Integration tests are run as gRPC clients against a running ZITADEL server binar The server binary is typically [build with coverage enabled](https://go.dev/doc/build-cover). It is also possible to run a ZITADEL sever in a debugger and run the integrations tests like that. In order to run the server, a database is required. -The database flavor can **optionally** be set in the environment to `cockroach` or `postgres`. The default is `postgres`. - -```bash -export INTEGRATION_DB_FLAVOR="cockroach" -``` - In order to prepare the local system, the following will bring up the database, builds a coverage binary, initializes the database and starts the sever. ```bash @@ -299,7 +300,7 @@ docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml down ### Console By executing the commands from this section, you run everything you need to develop the console locally. -Using [Docker Compose](https://docs.docker.com/compose/), you run [CockroachDB](https://www.cockroachlabs.com/docs/stable/start-a-local-cluster-in-docker-mac.html) and the [latest release of ZITADEL](https://github.com/zitadel/zitadel/releases/latest) on your local machine. +Using [Docker Compose](https://docs.docker.com/compose/), you run [PostgreSQL](https://www.postgresql.org/download/) and the [latest release of ZITADEL](https://github.com/zitadel/zitadel/releases/latest) on your local machine. You use the ZITADEL container as backend for your console. The console is run in your [Node](https://nodejs.org/en/about/) environment using [a local development server for Angular](https://angular.io/cli/serve#ng-serve), so you have fast feedback about your changes. diff --git a/LICENSE b/LICENSE index a1ea99bb88..bae94e189e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. - 1. Definitions. + Preamble - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + The precise terms and conditions for copying, distribution and +modification follow. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + TERMS AND CONDITIONS - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + 0. Definitions. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "This License" refers to version 3 of the GNU Affero General Public License. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + A "covered work" means either the unmodified Program or a work based +on the Program. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + 1. Source Code. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. - END OF TERMS AND CONDITIONS + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. - APPENDIX: How to apply the Apache License to your work. + The Corresponding Source for a work in source code form is that +same work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + 2. Basic Permissions. - Copyright 2020 CAOS AG + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. - http://www.apache.org/licenses/LICENSE-2.0 + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000000..9cad2082f8 --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,25 @@ +# Licensing Policy + +This repository is licensed under the [GNU Affero General Public License v3.0](LICENSE) (AGPL-3.0-only). We use the [SPDX License List](https://spdx.org/licenses/) for standard license naming. + +## AGPL-3.0-only Compliance + +ZITADEL is open-source software intended for community use. Determining your application's compliance with the AGPL-3.0-only license is your responsibility. + +**We strongly recommend consulting with legal counsel or licensing specialists to ensure your usage of ZITADEL, and any other integrated open-source projects, adheres to their respective licenses. AGPL-3.0-only compliance can be complex.** + +If your application triggers AGPL-3.0-only obligations and you wish to avoid them (e.g., you do not plan to open-source your modifications or application), please [contact us](https://zitadel.com/contact) to discuss commercial licensing options. Using ZITADEL without verifying your license compliance is at your own risk. + +## Exceptions to AGPL-3.0-only + +The following files and directories, including their subdirectories, are licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0): + +``` +proto/ +``` + +## Community Contributions + +To maintain a clear licensing structure and facilitate community contributions, all contributions must be licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) to be accepted. By submitting a contribution, you agree to this licensing. + +This approach avoids the need for a Contributor License Agreement (CLA) while ensuring clarity regarding license terms. We will only accept contributions licensed under Apache 2.0. diff --git a/Makefile b/Makefile index 27e76c0614..3c50231bee 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,9 @@ COMMIT_SHA ?= $(shell git rev-parse HEAD) ZITADEL_IMAGE ?= zitadel:local GOCOVERDIR = tmp/coverage -INTEGRATION_DB_FLAVOR ?= postgres ZITADEL_MASTERKEY ?= MasterkeyNeedsToHave32Characters -export GOCOVERDIR INTEGRATION_DB_FLAVOR ZITADEL_MASTERKEY +export GOCOVERDIR ZITADEL_MASTERKEY .PHONY: compile compile: core_build console_build compile_pipeline @@ -113,7 +112,7 @@ core_unit_test: .PHONY: core_integration_db_up core_integration_db_up: - docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait $${INTEGRATION_DB_FLAVOR} cache + docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait cache postgres .PHONY: core_integration_db_down core_integration_db_down: @@ -123,13 +122,13 @@ core_integration_db_down: core_integration_setup: go build -cover -race -tags integration -o zitadel.test main.go mkdir -p $${GOCOVERDIR} - GORACE="halt_on_error=1" ./zitadel.test init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml - GORACE="halt_on_error=1" ./zitadel.test setup --masterkeyFromEnv --init-projections --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml --steps internal/integration/config/steps.yaml + GORACE="halt_on_error=1" ./zitadel.test init --config internal/integration/config/zitadel.yaml --config internal/integration/config/postgres.yaml + GORACE="halt_on_error=1" ./zitadel.test setup --masterkeyFromEnv --init-projections --config internal/integration/config/zitadel.yaml --config internal/integration/config/postgres.yaml --steps internal/integration/config/steps.yaml .PHONY: core_integration_server_start core_integration_server_start: core_integration_setup GORACE="log_path=tmp/race.log" \ - ./zitadel.test start --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml \ + ./zitadel.test start --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/postgres.yaml \ > tmp/zitadel.log 2>&1 \ & printf $$! > tmp/zitadel.pid diff --git a/README.md b/README.md index 592952cdc2..285e50964c 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ We provide you with a wide range of out-of-the-box features to accelerate your p :white_check_mark: LDAP :white_check_mark: Passkeys / FIDO2 :white_check_mark: OTP +:white_check_mark: SCIM 2.0 Server and an unlimited audit trail is there for you, ready to use. With ZITADEL, you are assured of a robust and customizable turnkey solution for all your authentication and authorization needs. @@ -106,7 +107,7 @@ Yet it offers everything you need for a customer identity ([CIAM](https://zitade - [Actions](https://zitadel.com/docs/apis/actions/introduction) to react on events with custom code and extended ZITADEL for you needs - [Branding](https://zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience across multiple organizations - [Self-service](https://zitadel.com/docs/concepts/features/selfservice) for end-users, business customers, and administrators -- [CockroachDB](https://www.cockroachlabs.com/) or a [Postgres](https://www.postgresql.org/) database as reliable and widespread storage option +- [Postgres](https://www.postgresql.org/) database as reliable and widespread storage option ## Features @@ -124,6 +125,7 @@ Authentication - [Custom sessions](https://zitadel.com/docs/guides/integrate/login-ui/username-password) if you need to go beyond OIDC or SAML - [Machine-to-machine](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users) with JWT profile, Personal Access Tokens (PAT), and Client Credentials - [Token exchange and impersonation](https://zitadel.com/docs/guides/integrate/token-exchange) +- [Beta: Hosted Login V2](https://zitadel.com/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) our new login version 2.0 Multi-Tenancy @@ -137,10 +139,11 @@ Integration - [GRPC and REST APIs](https://zitadel.com/docs/apis/introduction) for every functionality and resource - [Actions](https://zitadel.com/docs/apis/actions/introduction) to call any API, send webhooks, adjust workflows, or customize tokens - [Role Based Access Control (RBAC)](https://zitadel.com/docs/guides/integrate/retrieve-user-roles) +- [SCIM 2.0 Server](https://zitadel.com/docs/apis/scim2) - [Examples and SDKs](https://zitadel.com/docs/sdk-examples/introduction) - [Audit Log and SOC/SIEM](https://zitadel.com/docs/guides/integrate/external-audit-log) - [User registration and onboarding](https://zitadel.com/docs/guides/integrate/onboarding) -- [Hosted and custom login user interface](https://zitadel.com/docs/guides/integrate/login-ui) +- [Hosted and custom login user interface](https://zitadel.com/docs/guides/integrate/login/login-users) Self-Service - [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification @@ -148,7 +151,7 @@ Self-Service - [Administration UI (Console)](https://zitadel.com/docs/guides/manage/console/overview) Deployment -- [Postgres](https://zitadel.com/docs/self-hosting/manage/database#postgres) (version >= 14) or [CockroachDB](https://zitadel.com/docs/self-hosting/manage/database#cockroach) (version latest stable) +- [Postgres](https://zitadel.com/docs/self-hosting/manage/database#postgres) (version >= 14) - [Zero Downtime Updates](https://zitadel.com/docs/concepts/architecture/solution#zero-downtime-updates) - [High scalability](https://zitadel.com/docs/self-hosting/manage/production) @@ -165,7 +168,7 @@ Join our [Discord Chat](https://zitadel.com/chat) to get help. -Made with [contrib.rocks](https://contrib.rocks). +Made with [contrib.rocks](https://contrib.rocks/preview?repo=zitadel/zitadel). ## Showcase @@ -187,6 +190,11 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A [![Console Showcase](https://user-images.githubusercontent.com/1366906/223663344-67038d5f-4415-4285-ab20-9a4d397e2138.gif)](http://www.youtube.com/watch?v=RPpHktAcCtk "Console Showcase") +### Login V2 + +Check out our new Login V2 version in our [typescript repository](https://github.com/zitadel/typescript) or in our [documentation](https://zitadel.com/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) +[![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26)] + ## Security You can find our security policy [here](./SECURITY.md). diff --git a/build/Dockerfile b/build/Dockerfile index 4e984fe8e6..769f04023e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -27,6 +27,7 @@ COPY --from=artifact /etc/ssl/certs /etc/ssl/certs COPY --from=artifact /app/zitadel /app/zitadel HEALTHCHECK NONE +EXPOSE 8080 USER zitadel -ENTRYPOINT ["/app/zitadel"] \ No newline at end of file +ENTRYPOINT ["/app/zitadel"] diff --git a/build/workflow.Dockerfile b/build/workflow.Dockerfile index db27daf91c..2286531192 100644 --- a/build/workflow.Dockerfile +++ b/build/workflow.Dockerfile @@ -199,7 +199,6 @@ ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" WORKDIR /go/src/github.com/zitadel/zitadel # default vars -ENV DB_FLAVOR=postgres ENV POSTGRES_USER=zitadel ENV POSTGRES_DB=zitadel ENV POSTGRES_PASSWORD=postgres @@ -231,12 +230,6 @@ COPY --from=test-core-unit /go/src/github.com/zitadel/zitadel/profile.cov /cover # integration test core # ####################################### FROM test-core-base AS test-core-integration -ENV DB_FLAVOR=cockroach - -# install cockroach -COPY --from=cockroachdb/cockroach:latest /cockroach/cockroach /usr/local/bin/ -ENV COCKROACH_BINARY=/cockroach/cockroach - ENV ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters COPY build/core-integration-test.sh /usr/local/bin/run-tests.sh diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 3615c7fa34..8482ccec9f 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -14,6 +14,7 @@ Tracing: # for type 'otel' is used for standard [open telemetry](https://opentelemetry.io) # Fraction: 1.0 # Endpoint: 'otel.collector.endpoint' + # ServiceName: 'ZITADEL' # Name of the service in traces # # type 'log' or '' disables tracing # @@ -24,6 +25,8 @@ Tracing: Fraction: 1.0 # ZITADEL_TRACING_FRACTION # The endpoint of the otel collector endpoint Endpoint: "" #ZITADEL_TRACING_ENDPOINT + # The name of the service in traces + ServiceName: "ZITADEL" #ZITADEL_TRACING_SERVICENAME # Profiler enables capturing profiling data (CPU, Memory, ...) for performance analysis Profiler: @@ -110,67 +113,36 @@ PublicHostHeaders: # ZITADEL_PUBLICHOSTHEADERS WebAuthNName: ZITADEL # ZITADEL_WEBAUTHNNAME Database: - # CockroachDB is the default database of ZITADEL - cockroach: - Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST - Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT - Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE - MaxOpenConns: 5 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS - MaxIdleConns: 2 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME - Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS + # Postgres is the default database of ZITADEL + postgres: + Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST + Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT + Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE + MaxOpenConns: 10 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS + MaxIdleConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS User: - Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD + Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD SSL: - Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY Admin: # By default, ExistingDatabase is not specified in the connection string # If the connection resolves to a database that is not existing in your system, configure an existing one here - # It is used in zitadel init to connect to cockroach and create a dedicated database for ZITADEL. - ExistingDatabase: # ZITADEL_DATABASE_COCKROACH_ADMIN_EXISTINGDATABASE - Username: root # ZITADEL_DATABASE_COCKROACH_ADMIN_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_PASSWORD - SSL: - Mode: disable # ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_CERT - Key: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_KEY - # Postgres is used as soon as a value is set - # The values describe the possible fields to set values - postgres: - Host: # ZITADEL_DATABASE_POSTGRES_HOST - Port: # ZITADEL_DATABASE_POSTGRES_PORT - Database: # ZITADEL_DATABASE_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS - User: - Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME - Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD - SSL: - Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE - RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT - Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT - Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY - Admin: - # The default ExistingDatabase is postgres - # If your db system doesn't have a database named postgres, configure an existing database here # It is used in zitadel init to connect to postgres and create a dedicated database for ZITADEL. ExistingDatabase: # ZITADEL_DATABASE_POSTGRES_ADMIN_EXISTINGDATABASE - Username: # ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME - Password: # ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD + Username: postgres # ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME + Password: postgres # ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD SSL: - Mode: # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE - RootCert: # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_ROOTCERT - Cert: # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_CERT - Key: # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_KEY # Caches are EXPERIMENTAL. The following config may have breaking changes in the future. # If no config is provided, caching is disabled by default. @@ -416,12 +388,10 @@ Projections: TransactionDuration: 0s BulkLimit: 2000 - # The Notifications projection is used for sending emails and SMS to users + # The Notifications projection is used for preparing the messages (emails and SMS) to be sent to users Notifications: # As notification projections don't result in database statements, retries don't have an effect MaxFailureCount: 10 # ZITADEL_PROJECTIONS_CUSTOMIZATIONS_NOTIFICATIONS_MAXFAILURECOUNT - # Sending emails can take longer than 500ms - TransactionDuration: 5s # ZITADEL_PROJECTIONS_CUSTOMIZATIONS_NOTIFICATIONS_TRANSACTIONDURATION password_complexities: TransactionDuration: 2s # ZITADEL_PROJECTIONS_CUSTOMIZATIONS_PASSWORD_COMPLEXITIES_TRANSACTIONDURATION lockout_policy: @@ -446,41 +416,30 @@ Projections: Notifications: # Notifications can be processed by either a sequential mode (legacy) or a new parallel mode. # The parallel mode is currently only recommended for Postgres databases. - # For CockroachDB, the sequential mode is recommended, see: https://github.com/zitadel/zitadel/issues/9002 # If legacy mode is enabled, the worker config below is ignored. LegacyEnabled: true # ZITADEL_NOTIFICATIONS_LEGACYENABLED # The amount of workers processing the notification request events. # If set to 0, no notification request events will be handled. This can be useful when running in # multi binary / pod setup and allowing only certain executables to process the events. - Workers: 1 # ZITADEL_NOTIFIACATIONS_WORKERS - # The amount of events a single worker will process in a run. - BulkLimit: 10 # ZITADEL_NOTIFIACATIONS_BULKLIMIT - # Time interval between scheduled notifications for request events - RequeueEvery: 5s # ZITADEL_NOTIFIACATIONS_REQUEUEEVERY - # The amount of workers processing the notification retry events. - # If set to 0, no notification retry events will be handled. This can be useful when running in - # multi binary / pod setup and allowing only certain executables to process the events. - RetryWorkers: 1 # ZITADEL_NOTIFIACATIONS_RETRYWORKERS - # Time interval between scheduled notifications for retry events - RetryRequeueEvery: 5s # ZITADEL_NOTIFIACATIONS_RETRYREQUEUEEVERY - # Only instances are projected, for which at least a projection-relevant event exists within the timeframe - # from HandleActiveInstances duration in the past until the projection's current time - # If set to 0 (default), every instance is always considered active - HandleActiveInstances: 0s # ZITADEL_NOTIFIACATIONS_HANDLEACTIVEINSTANCES - # The maximum duration a transaction remains open - # before it spots left folding additional events - # and updates the table. - TransactionDuration: 10s # ZITADEL_NOTIFIACATIONS_TRANSACTIONDURATION + Workers: 1 # ZITADEL_NOTIFICATIONS_WORKERS + # The maximum duration a job can do it's work before it is considered as failed. + TransactionDuration: 10s # ZITADEL_NOTIFICATIONS_TRANSACTIONDURATION # Automatically cancel the notification after the amount of failed attempts - MaxAttempts: 3 # ZITADEL_NOTIFIACATIONS_MAXATTEMPTS + MaxAttempts: 3 # ZITADEL_NOTIFICATIONS_MAXATTEMPTS # Automatically cancel the notification if it cannot be handled within a specific time - MaxTtl: 5m # ZITADEL_NOTIFIACATIONS_MAXTTL - # Failed attempts are retried after a confogired delay (with exponential backoff). - # Set a minimum and maximum delay and a factor for the backoff - MinRetryDelay: 5s # ZITADEL_NOTIFIACATIONS_MINRETRYDELAY - MaxRetryDelay: 1m # ZITADEL_NOTIFIACATIONS_MAXRETRYDELAY - # Any factor below 1 will be set to 1 - RetryDelayFactor: 1.5 # ZITADEL_NOTIFIACATIONS_RETRYDELAYFACTOR + MaxTtl: 5m # ZITADEL_NOTIFICATIONS_MAXTTL + +Executions: + # The amount of workers processing the execution request events. + # If set to 0, no execution request events will be handled. This can be useful when running in + # multi binary / pod setup and allowing only certain executables to process the events. + Workers: 1 # ZITADEL_EXECUTIONS_WORKERS + # The maximum duration a job can do it's work before it is considered as failed. + # This maximum duration is prioritized in case that the sum of the target's timeouts is higher, + # to limit the runtime of a singular execution. + TransactionDuration: 10s # ZITADEL_EXECUTIONS_TRANSACTIONDURATION + # Automatically cancel the notification if it cannot be handled within a specific time + MaxTtl: 5m # ZITADEL_EXECUTIONS_MAXTTL Auth: # See Projections.BulkLimit @@ -648,6 +607,16 @@ EncryptionKeys: UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID SystemAPIUsers: + - superuser: + Path: /path/to/superuser/key.pem + Memberships: + - MemberType: Organization + Roles: "ORG_OWNER" + AggregateID: "123456789012345678" + - MemberType: Project + Roles: "PROJECT_OWNER" + + # # Add keys for authentication of the systemAPI here: # # you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT: # - superuser: @@ -718,6 +687,7 @@ SystemDefaults: # - "bcrypt" # - "md5" # md5Crypt with salt and password shuffling. # - "md5plain" # md5 digest of a password without salt + # - "md5salted" # md5 digest of a salted password # - "scrypt" # - "pbkdf2" # verifier for all pbkdf2 hash modes. SecretHasher: @@ -1321,6 +1291,8 @@ InternalAuthZ: - "userschema.read" - "userschema.write" - "userschema.delete" + - "session.read" + - "session.delete" - Role: "IAM_OWNER_VIEWER" Permissions: - "iam.read" @@ -1356,6 +1328,7 @@ InternalAuthZ: - "action.target.read" - "action.execution.read" - "userschema.read" + - "session.read" - Role: "IAM_ORG_MANAGER" Permissions: - "org.read" @@ -1750,6 +1723,298 @@ InternalAuthZ: - "user.grant.read" - "user.membership.read" +SystemAuthZ: + RolePermissionMappings: + - Role: "SYSTEM_OWNER" + Permissions: + - "system.instance.read" + - "system.instance.write" + - "system.instance.delete" + - "system.domain.read" + - "system.domain.write" + - "system.domain.delete" + - "system.debug.read" + - "system.debug.write" + - "system.debug.delete" + - "system.feature.read" + - "system.feature.write" + - "system.feature.delete" + - "system.limits.write" + - "system.limits.delete" + - "system.quota.write" + - "system.quota.delete" + - "system.iam.member.read" + - Role: "SYSTEM_OWNER_VIEWER" + Permissions: + - "system.instance.read" + - "system.domain.read" + - "system.debug.read" + - "system.feature.read" + - "system.iam.member.read" + - Role: "IAM_OWNER" + Permissions: + - "iam.read" + - "iam.write" + - "iam.policy.read" + - "iam.policy.write" + - "iam.policy.delete" + - "iam.member.read" + - "iam.member.write" + - "iam.member.delete" + - "iam.idp.read" + - "iam.idp.write" + - "iam.idp.delete" + - "iam.action.read" + - "iam.action.write" + - "iam.action.delete" + - "iam.flow.read" + - "iam.flow.write" + - "iam.flow.delete" + - "iam.feature.read" + - "iam.feature.write" + - "iam.feature.delete" + - "iam.restrictions.read" + - "iam.restrictions.write" + - "iam.web_key.write" + - "iam.web_key.delete" + - "iam.web_key.read" + - "iam.debug.write" + - "iam.debug.read" + - "org.read" + - "org.global.read" + - "org.create" + - "org.write" + - "org.delete" + - "org.member.read" + - "org.member.write" + - "org.member.delete" + - "org.idp.read" + - "org.idp.write" + - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" + - "org.feature.read" + - "org.feature.write" + - "org.feature.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "policy.read" + - "policy.write" + - "policy.delete" + - "project.read" + - "project.create" + - "project.write" + - "project.delete" + - "project.member.read" + - "project.member.write" + - "project.member.delete" + - "project.role.read" + - "project.role.write" + - "project.role.delete" + - "project.app.read" + - "project.app.write" + - "project.app.delete" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "project.grant.member.write" + - "project.grant.member.delete" + - "events.read" + - "milestones.read" + - "session.read" + - "session.delete" + - "action.target.read" + - "action.target.write" + - "action.target.delete" + - "action.execution.read" + - "action.execution.write" + - "userschema.read" + - "userschema.write" + - "userschema.delete" + - "session.read" + - "session.delete" + - Role: "IAM_OWNER_VIEWER" + Permissions: + - "iam.read" + - "iam.policy.read" + - "iam.member.read" + - "iam.idp.read" + - "iam.action.read" + - "iam.flow.read" + - "iam.restrictions.read" + - "iam.feature.read" + - "iam.web_key.read" + - "iam.debug.read" + - "org.read" + - "org.member.read" + - "org.idp.read" + - "org.action.read" + - "org.flow.read" + - "org.feature.read" + - "user.read" + - "user.global.read" + - "user.grant.read" + - "user.membership.read" + - "user.feature.read" + - "policy.read" + - "project.read" + - "project.member.read" + - "project.role.read" + - "project.app.read" + - "project.grant.read" + - "project.grant.member.read" + - "events.read" + - "milestones.read" + - "action.target.read" + - "action.execution.read" + - "userschema.read" + - "session.read" + - Role: "IAM_ORG_MANAGER" + Permissions: + - "org.read" + - "org.global.read" + - "org.create" + - "org.write" + - "org.delete" + - "org.member.read" + - "org.member.write" + - "org.member.delete" + - "org.idp.read" + - "org.idp.write" + - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" + - "org.feature.read" + - "org.feature.write" + - "org.feature.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "policy.read" + - "policy.write" + - "policy.delete" + - "project.read" + - "project.create" + - "project.write" + - "project.delete" + - "project.member.read" + - "project.member.write" + - "project.member.delete" + - "project.role.read" + - "project.role.write" + - "project.role.delete" + - "project.app.read" + - "project.app.write" + - "project.app.delete" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "project.grant.member.write" + - "project.grant.member.delete" + - "session.delete" + - Role: "IAM_USER_MANAGER" + Permissions: + - "org.read" + - "org.global.read" + - "org.member.read" + - "org.member.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "project.read" + - "project.member.read" + - "project.role.read" + - "project.app.read" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "session.delete" + - Role: "IAM_ADMIN_IMPERSONATOR" + Permissions: + - "admin.impersonation" + - "impersonation" + - Role: "IAM_END_USER_IMPERSONATOR" + Permissions: + - "impersonation" + - Role: "IAM_LOGIN_CLIENT" + Permissions: + - "iam.read" + - "iam.policy.read" + - "iam.member.read" + - "iam.member.write" + - "iam.idp.read" + - "iam.feature.read" + - "iam.restrictions.read" + - "org.read" + - "org.member.read" + - "org.member.write" + - "org.idp.read" + - "org.feature.read" + - "user.read" + - "user.write" + - "user.grant.read" + - "user.grant.write" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "policy.read" + - "project.read" + - "project.member.read" + - "project.member.write" + - "project.role.read" + - "project.app.read" + - "project.member.read" + - "project.member.write" + - "project.grant.read" + - "project.grant.member.read" + - "project.grant.member.write" + - "session.read" + - "session.link" + - "session.delete" + - "userschema.read" + # If a new projection is introduced it will be prefilled during the setup process (if enabled) # This can prevent serving outdated data after a version upgrade, but might require a longer setup / upgrade process: # https://zitadel.com/docs/self-hosting/manage/updating_scaling diff --git a/cmd/initialise/config.go b/cmd/initialise/config.go index 3fe7173860..899018ddcb 100644 --- a/cmd/initialise/config.go +++ b/cmd/initialise/config.go @@ -19,7 +19,7 @@ func MustNewConfig(v *viper.Viper) *Config { config := new(Config) err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( - database.DecodeHook, + database.DecodeHook(false), mapstructure.TextUnmarshallerHookFunc(), )), ) diff --git a/cmd/initialise/init.go b/cmd/initialise/init.go index 02fd481eab..cc505325a9 100644 --- a/cmd/initialise/init.go +++ b/cmd/initialise/init.go @@ -12,20 +12,17 @@ import ( ) var ( - //go:embed sql/cockroach/* - //go:embed sql/postgres/* + //go:embed sql/*.sql stmts embed.FS createUserStmt string grantStmt string - settingsStmt string databaseStmt string createEventstoreStmt string createProjectionsStmt string createSystemStmt string createEncryptionKeysStmt string createEventsStmt string - createSystemSequenceStmt string createUniqueConstraints string roleAlreadyExistsCode = "42710" @@ -39,7 +36,7 @@ func New() *cobra.Command { Long: `Sets up the minimum requirements to start ZITADEL. Prerequisites: -- database (PostgreSql or cockroachdb) +- PostgreSql database The user provided by flags needs privileges to - create the database if it does not exist @@ -53,7 +50,7 @@ The user provided by flags needs privileges to }, } - cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant(), newSettings()) + cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant()) return cmd } @@ -62,7 +59,6 @@ func InitAll(ctx context.Context, config *Config) { VerifyUser(config.Database.Username(), config.Database.Password()), VerifyDatabase(config.Database.DatabaseName()), VerifyGrant(config.Database.DatabaseName(), config.Database.Username()), - VerifySettings(config.Database.DatabaseName(), config.Database.Username()), ) logging.OnError(err).Fatal("unable to initialize the database") @@ -73,7 +69,7 @@ func InitAll(ctx context.Context, config *Config) { func initialise(ctx context.Context, config database.Config, steps ...func(context.Context, *database.DB) error) error { logging.Info("initialization started") - err := ReadStmts(config.Type()) + err := ReadStmts() if err != nil { return err } @@ -97,58 +93,48 @@ func Init(ctx context.Context, db *database.DB, steps ...func(context.Context, * return nil } -func ReadStmts(typ string) (err error) { - createUserStmt, err = readStmt(typ, "01_user") +func ReadStmts() (err error) { + createUserStmt, err = readStmt("01_user") if err != nil { return err } - databaseStmt, err = readStmt(typ, "02_database") + databaseStmt, err = readStmt("02_database") if err != nil { return err } - grantStmt, err = readStmt(typ, "03_grant_user") + grantStmt, err = readStmt("03_grant_user") if err != nil { return err } - createEventstoreStmt, err = readStmt(typ, "04_eventstore") + createEventstoreStmt, err = readStmt("04_eventstore") if err != nil { return err } - createProjectionsStmt, err = readStmt(typ, "05_projections") + createProjectionsStmt, err = readStmt("05_projections") if err != nil { return err } - createSystemStmt, err = readStmt(typ, "06_system") + createSystemStmt, err = readStmt("06_system") if err != nil { return err } - createEncryptionKeysStmt, err = readStmt(typ, "07_encryption_keys_table") + createEncryptionKeysStmt, err = readStmt("07_encryption_keys_table") if err != nil { return err } - createEventsStmt, err = readStmt(typ, "08_events_table") + createEventsStmt, err = readStmt("08_events_table") if err != nil { return err } - createSystemSequenceStmt, err = readStmt(typ, "09_system_sequence") - if err != nil { - return err - } - - createUniqueConstraints, err = readStmt(typ, "10_unique_constraints_table") - if err != nil { - return err - } - - settingsStmt, err = readStmt(typ, "11_settings") + createUniqueConstraints, err = readStmt("10_unique_constraints_table") if err != nil { return err } @@ -156,7 +142,7 @@ func ReadStmts(typ string) (err error) { return nil } -func readStmt(typ, step string) (string, error) { - stmt, err := stmts.ReadFile("sql/" + typ + "/" + step + ".sql") +func readStmt(step string) (string, error) { + stmt, err := stmts.ReadFile("sql/" + step + ".sql") return string(stmt), err } diff --git a/cmd/initialise/sql/cockroach/01_user.sql b/cmd/initialise/sql/01_user.sql similarity index 56% rename from cmd/initialise/sql/cockroach/01_user.sql rename to cmd/initialise/sql/01_user.sql index 4e621216ce..7be2d4ae4d 100644 --- a/cmd/initialise/sql/cockroach/01_user.sql +++ b/cmd/initialise/sql/01_user.sql @@ -1,2 +1,2 @@ -- replace %[1]s with the name of the user -CREATE USER IF NOT EXISTS "%[1]s" \ No newline at end of file +CREATE USER "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/02_database.sql b/cmd/initialise/sql/02_database.sql similarity index 54% rename from cmd/initialise/sql/cockroach/02_database.sql rename to cmd/initialise/sql/02_database.sql index 6103b95b31..172913661b 100644 --- a/cmd/initialise/sql/cockroach/02_database.sql +++ b/cmd/initialise/sql/02_database.sql @@ -1,2 +1,2 @@ -- replace %[1]s with the name of the database -CREATE DATABASE IF NOT EXISTS "%[1]s"; +CREATE DATABASE "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/03_grant_user.sql b/cmd/initialise/sql/03_grant_user.sql similarity index 100% rename from cmd/initialise/sql/postgres/03_grant_user.sql rename to cmd/initialise/sql/03_grant_user.sql diff --git a/cmd/initialise/sql/cockroach/04_eventstore.sql b/cmd/initialise/sql/04_eventstore.sql similarity index 100% rename from cmd/initialise/sql/cockroach/04_eventstore.sql rename to cmd/initialise/sql/04_eventstore.sql diff --git a/cmd/initialise/sql/cockroach/05_projections.sql b/cmd/initialise/sql/05_projections.sql similarity index 100% rename from cmd/initialise/sql/cockroach/05_projections.sql rename to cmd/initialise/sql/05_projections.sql diff --git a/cmd/initialise/sql/cockroach/06_system.sql b/cmd/initialise/sql/06_system.sql similarity index 100% rename from cmd/initialise/sql/cockroach/06_system.sql rename to cmd/initialise/sql/06_system.sql diff --git a/cmd/initialise/sql/cockroach/07_encryption_keys_table.sql b/cmd/initialise/sql/07_encryption_keys_table.sql similarity index 100% rename from cmd/initialise/sql/cockroach/07_encryption_keys_table.sql rename to cmd/initialise/sql/07_encryption_keys_table.sql diff --git a/cmd/initialise/sql/postgres/08_events_table.sql b/cmd/initialise/sql/08_events_table.sql similarity index 100% rename from cmd/initialise/sql/postgres/08_events_table.sql rename to cmd/initialise/sql/08_events_table.sql diff --git a/cmd/initialise/sql/postgres/10_unique_constraints_table.sql b/cmd/initialise/sql/10_unique_constraints_table.sql similarity index 100% rename from cmd/initialise/sql/postgres/10_unique_constraints_table.sql rename to cmd/initialise/sql/10_unique_constraints_table.sql diff --git a/cmd/initialise/sql/README.md b/cmd/initialise/sql/README.md index b477c0fb73..b7a18f0f98 100644 --- a/cmd/initialise/sql/README.md +++ b/cmd/initialise/sql/README.md @@ -11,6 +11,5 @@ The sql-files in this folder initialize the ZITADEL database and user. These obj - 05_projections.sql: creates the schema needed to read the data - 06_system.sql: creates the schema needed for ZITADEL itself - 07_encryption_keys_table.sql: creates the table for encryption keys (for event data) -- files 08_enable_hash_sharded_indexes.sql and 09_events_table.sql must run in the same session - - 08_enable_hash_sharded_indexes.sql enables the [hash sharded index](https://www.cockroachlabs.com/docs/stable/hash-sharded-indexes.html) feature for this session - - 09_events_table.sql creates the table for eventsourcing +- 08_events_table.sql creates the table for eventsourcing +- 10_unique_constraints_table.sql creates the table to check unique constraints for events diff --git a/cmd/initialise/sql/cockroach/03_grant_user.sql b/cmd/initialise/sql/cockroach/03_grant_user.sql deleted file mode 100644 index de0d2743eb..0000000000 --- a/cmd/initialise/sql/cockroach/03_grant_user.sql +++ /dev/null @@ -1,4 +0,0 @@ --- replace the first %[1]s with the database --- replace the second \%[2]s with the user -GRANT ALL ON DATABASE "%[1]s" TO "%[2]s"; -GRANT SYSTEM VIEWACTIVITY TO "%[2]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/08_events_table.sql b/cmd/initialise/sql/cockroach/08_events_table.sql deleted file mode 100644 index ebaf18ce2a..0000000000 --- a/cmd/initialise/sql/cockroach/08_events_table.sql +++ /dev/null @@ -1,116 +0,0 @@ -CREATE TABLE IF NOT EXISTS eventstore.events2 ( - instance_id TEXT NOT NULL - , aggregate_type TEXT NOT NULL - , aggregate_id TEXT NOT NULL - - , event_type TEXT NOT NULL - , "sequence" BIGINT NOT NULL - , revision SMALLINT NOT NULL - , created_at TIMESTAMPTZ NOT NULL - , payload JSONB - , creator TEXT NOT NULL - , "owner" TEXT NOT NULL - - , "position" DECIMAL NOT NULL - , in_tx_order INTEGER NOT NULL - - , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, "sequence") - , INDEX es_active_instances (created_at DESC) STORING ("position") - , INDEX es_wm (aggregate_id, instance_id, aggregate_type, event_type) - , INDEX es_projection (instance_id, aggregate_type, event_type, "position" DESC) -); - --- represents an event to be created. -CREATE TYPE IF NOT EXISTS eventstore.command AS ( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - , command_type TEXT - , revision INT2 - , payload JSONB - , creator TEXT - , owner TEXT -); - -CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$ -SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type AS event_type - , cs.sequence + ROW_NUMBER() OVER (PARTITION BY ("c").instance_id, ("c").aggregate_type, ("c").aggregate_id ORDER BY ("c").in_tx_order) AS sequence - , ("c").revision - , hlc_to_timestamp(cluster_logical_timestamp()) AS created_at - , ("c").payload - , ("c").creator - , cs.owner - , cluster_logical_timestamp() AS position - , ("c").in_tx_order -FROM ( - SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type - , ("c").revision - , ("c").payload - , ("c").creator - , ("c").owner - , ROW_NUMBER() OVER () AS in_tx_order - FROM - UNNEST(commands) AS "c" -) AS "c" -JOIN ( - SELECT - cmds.instance_id - , cmds.aggregate_type - , cmds.aggregate_id - , CASE WHEN (e.owner IS NOT NULL OR e.owner <> '') THEN e.owner ELSE command_owners.owner END AS owner - , COALESCE(MAX(e.sequence), 0) AS sequence - FROM ( - SELECT DISTINCT - ("cmds").instance_id - , ("cmds").aggregate_type - , ("cmds").aggregate_id - , ("cmds").owner - FROM UNNEST(commands) AS "cmds" - ) AS cmds - LEFT JOIN eventstore.events2 AS e - ON cmds.instance_id = e.instance_id - AND cmds.aggregate_type = e.aggregate_type - AND cmds.aggregate_id = e.aggregate_id - JOIN ( - SELECT - DISTINCT ON ( - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - ) - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").owner - FROM - UNNEST(commands) AS "c" - ) AS command_owners ON - cmds.instance_id = command_owners.instance_id - AND cmds.aggregate_type = command_owners.aggregate_type - AND cmds.aggregate_id = command_owners.aggregate_id - GROUP BY - cmds.instance_id - , cmds.aggregate_type - , cmds.aggregate_id - , 4 -- owner -) AS cs - ON ("c").instance_id = cs.instance_id - AND ("c").aggregate_type = cs.aggregate_type - AND ("c").aggregate_id = cs.aggregate_id -ORDER BY - in_tx_order -$$ LANGUAGE SQL; - -CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 AS $$ - INSERT INTO eventstore.events2 - SELECT * FROM eventstore.commands_to_events(commands) - RETURNING * -$$ LANGUAGE SQL; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/09_system_sequence.sql b/cmd/initialise/sql/cockroach/09_system_sequence.sql deleted file mode 100644 index 596e887664..0000000000 --- a/cmd/initialise/sql/cockroach/09_system_sequence.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE SEQUENCE IF NOT EXISTS eventstore.system_seq diff --git a/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql b/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql deleted file mode 100644 index 2594a248b7..0000000000 --- a/cmd/initialise/sql/cockroach/10_unique_constraints_table.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS eventstore.unique_constraints ( - instance_id TEXT, - unique_type TEXT, - unique_field TEXT, - PRIMARY KEY (instance_id, unique_type, unique_field) -) diff --git a/cmd/initialise/sql/cockroach/11_settings.sql b/cmd/initialise/sql/cockroach/11_settings.sql deleted file mode 100644 index 5fa9dd72f6..0000000000 --- a/cmd/initialise/sql/cockroach/11_settings.sql +++ /dev/null @@ -1,4 +0,0 @@ --- replace the first %[1]q with the database in double quotes --- replace the second \%[2]q with the user in double quotes$ --- For more information see technical advisory 10009 (https://zitadel.com/docs/support/advisory/a10009) -ALTER ROLE %[2]q IN DATABASE %[1]q SET enable_durable_locking_for_serializable = on; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/01_user.sql b/cmd/initialise/sql/postgres/01_user.sql deleted file mode 100644 index cd60b9a2cf..0000000000 --- a/cmd/initialise/sql/postgres/01_user.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE USER "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/02_database.sql b/cmd/initialise/sql/postgres/02_database.sql deleted file mode 100644 index 895a1f29d5..0000000000 --- a/cmd/initialise/sql/postgres/02_database.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE DATABASE "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/04_eventstore.sql b/cmd/initialise/sql/postgres/04_eventstore.sql deleted file mode 100644 index 3cb4fc0d3e..0000000000 --- a/cmd/initialise/sql/postgres/04_eventstore.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS eventstore; - -GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/05_projections.sql b/cmd/initialise/sql/postgres/05_projections.sql deleted file mode 100644 index 91ca6662ee..0000000000 --- a/cmd/initialise/sql/postgres/05_projections.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS projections; - -GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/06_system.sql b/cmd/initialise/sql/postgres/06_system.sql deleted file mode 100644 index 6c9138918b..0000000000 --- a/cmd/initialise/sql/postgres/06_system.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS system; - -GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/07_encryption_keys_table.sql b/cmd/initialise/sql/postgres/07_encryption_keys_table.sql deleted file mode 100644 index 61cb617fdf..0000000000 --- a/cmd/initialise/sql/postgres/07_encryption_keys_table.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS system.encryption_keys ( - id TEXT NOT NULL - , key TEXT NOT NULL - - , PRIMARY KEY (id) -); diff --git a/cmd/initialise/sql/postgres/09_system_sequence.sql b/cmd/initialise/sql/postgres/09_system_sequence.sql deleted file mode 100644 index 15383b3878..0000000000 --- a/cmd/initialise/sql/postgres/09_system_sequence.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE SEQUENCE IF NOT EXISTS eventstore.system_seq; diff --git a/cmd/initialise/verify_database.go b/cmd/initialise/verify_database.go index 6e04e489f5..3e3bea9efa 100644 --- a/cmd/initialise/verify_database.go +++ b/cmd/initialise/verify_database.go @@ -19,7 +19,7 @@ func newDatabase() *cobra.Command { Long: `Sets up the ZITADEL database. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL The user provided by flags needs privileges to - create the database if it does not exist diff --git a/cmd/initialise/verify_database_test.go b/cmd/initialise/verify_database_test.go index d7da97847f..1899605e4f 100644 --- a/cmd/initialise/verify_database_test.go +++ b/cmd/initialise/verify_database_test.go @@ -8,7 +8,7 @@ import ( ) func Test_verifyDB(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() @@ -27,7 +27,7 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", sql.ErrTxDone), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", sql.ErrTxDone), ), database: "zitadel", }, @@ -37,7 +37,7 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create successful", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", nil), ), database: "zitadel", }, @@ -47,7 +47,7 @@ func Test_verifyDB(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", nil), ), database: "zitadel", }, diff --git a/cmd/initialise/verify_grant.go b/cmd/initialise/verify_grant.go index a14a495bff..27f0bd4d08 100644 --- a/cmd/initialise/verify_grant.go +++ b/cmd/initialise/verify_grant.go @@ -19,7 +19,7 @@ func newGrant() *cobra.Command { Long: `Sets ALL grant to the database user. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL `, Run: func(cmd *cobra.Command, args []string) { config := MustNewConfig(viper.GetViper()) diff --git a/cmd/initialise/verify_settings.go b/cmd/initialise/verify_settings.go deleted file mode 100644 index 6f4ba7c074..0000000000 --- a/cmd/initialise/verify_settings.go +++ /dev/null @@ -1,45 +0,0 @@ -package initialise - -import ( - "context" - _ "embed" - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/database" -) - -func newSettings() *cobra.Command { - return &cobra.Command{ - Use: "settings", - Short: "Ensures proper settings on the database", - Long: `Ensures proper settings on the database. - -Prerequisites: -- cockroachDB or postgreSQL - -Cockroach -- Sets enable_durable_locking_for_serializable to on for the zitadel user and database -`, - Run: func(cmd *cobra.Command, args []string) { - config := MustNewConfig(viper.GetViper()) - - err := initialise(cmd.Context(), config.Database, VerifySettings(config.Database.DatabaseName(), config.Database.Username())) - logging.OnError(err).Fatal("unable to set settings") - }, - } -} - -func VerifySettings(databaseName, username string) func(context.Context, *database.DB) error { - return func(ctx context.Context, db *database.DB) error { - if db.Type() == "postgres" { - return nil - } - logging.WithFields("user", username, "database", databaseName).Info("verify settings") - - return exec(ctx, db, fmt.Sprintf(settingsStmt, databaseName, username), nil) - } -} diff --git a/cmd/initialise/verify_user.go b/cmd/initialise/verify_user.go index 43bdb91420..3adca93e53 100644 --- a/cmd/initialise/verify_user.go +++ b/cmd/initialise/verify_user.go @@ -19,7 +19,7 @@ func newUser() *cobra.Command { Long: `Sets up the ZITADEL database user. Prerequisites: -- cockroachDB or postgreSQL +- postgreSQL The user provided by flags needs privileges to - create the database if it does not exist diff --git a/cmd/initialise/verify_user_test.go b/cmd/initialise/verify_user_test.go index 53b35e67db..40cde5baa2 100644 --- a/cmd/initialise/verify_user_test.go +++ b/cmd/initialise/verify_user_test.go @@ -8,7 +8,7 @@ import ( ) func Test_verifyUser(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() @@ -28,7 +28,7 @@ func Test_verifyUser(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", sql.ErrTxDone), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", sql.ErrTxDone), ), username: "zitadel-user", password: "", @@ -39,7 +39,7 @@ func Test_verifyUser(t *testing.T) { name: "correct without password", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", nil), ), username: "zitadel-user", password: "", @@ -50,7 +50,7 @@ func Test_verifyUser(t *testing.T) { name: "correct with password", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", password: "password", @@ -61,7 +61,7 @@ func Test_verifyUser(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", password: "", diff --git a/cmd/initialise/verify_zitadel.go b/cmd/initialise/verify_zitadel.go index 1ae85a21fa..78f28809c2 100644 --- a/cmd/initialise/verify_zitadel.go +++ b/cmd/initialise/verify_zitadel.go @@ -21,7 +21,7 @@ func newZitadel() *cobra.Command { Long: `initialize ZITADEL internals. Prerequisites: -- cockroachDB or postgreSQL with user and database +- postgreSQL with user and database `, Run: func(cmd *cobra.Command, args []string) { config := MustNewConfig(viper.GetViper()) @@ -32,7 +32,7 @@ Prerequisites: } func VerifyZitadel(ctx context.Context, db *database.DB, config database.Config) error { - err := ReadStmts(config.Type()) + err := ReadStmts() if err != nil { return err } @@ -68,11 +68,6 @@ func VerifyZitadel(ctx context.Context, db *database.DB, config database.Config) return err } - logging.WithFields().Info("verify system sequence") - if err := exec(ctx, conn, createSystemSequenceStmt, nil); err != nil { - return err - } - logging.WithFields().Info("verify unique constraints") if err := exec(ctx, conn, createUniqueConstraints, nil); err != nil { return err diff --git a/cmd/initialise/verify_zitadel_test.go b/cmd/initialise/verify_zitadel_test.go index 194911a179..7fccd4c0a2 100644 --- a/cmd/initialise/verify_zitadel_test.go +++ b/cmd/initialise/verify_zitadel_test.go @@ -9,7 +9,7 @@ import ( ) func Test_verifyEvents(t *testing.T) { - err := ReadStmts("cockroach") //TODO: check all dialects + err := ReadStmts() if err != nil { t.Errorf("unable to read stmts: %v", err) t.FailNow() diff --git a/cmd/key/key.go b/cmd/key/key.go index 1dba8fd969..a1cf15b34e 100644 --- a/cmd/key/key.go +++ b/cmd/key/key.go @@ -40,7 +40,7 @@ func newKey() *cobra.Command { Long: `create new encryption key(s) (encrypted by the provided master key) provide key(s) by YAML file and/or by argument Requirements: -- cockroachdb`, +- postgreSQL`, Example: `new -f keys.yaml new key1=somekey key2=anotherkey new -f keys.yaml key2=anotherkey`, diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go index cc98000869..9d0113a1d7 100644 --- a/cmd/mirror/config.go +++ b/cmd/mirror/config.go @@ -16,6 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/id" + metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config" ) type Migration struct { @@ -26,6 +27,7 @@ type Migration struct { Log *logging.Config Machine *id.Config + Metrics metrics.Config } var ( @@ -40,6 +42,9 @@ func mustNewMigrationConfig(v *viper.Viper) *Migration { err := config.Log.SetLogger() logging.OnError(err).Fatal("unable to set logger") + err = config.Metrics.NewMeter() + logging.OnError(err).Fatal("unable to set meter") + id.Configure(config.Machine) return config @@ -71,7 +76,7 @@ func mustNewConfig(v *viper.Viper, config any) { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - database.DecodeHook, + database.DecodeHook(true), actions.HTTPConfigDecodeHook, hook.EnumHookFunc(internal_authz.MemberTypeString), mapstructure.TextUnmarshallerHookFunc(), diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml index 7db91ecc0b..4b42c06534 100644 --- a/cmd/mirror/defaults.yaml +++ b/cmd/mirror/defaults.yaml @@ -5,8 +5,6 @@ Source: Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE MaxOpenConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS MaxIdleConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - EventPushConnRatio: 0.33 # ZITADEL_DATABASE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.33 # ZITADEL_DATABASE_COCKROACH_PROJECTIONSPOOLERCONNRATIO MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS @@ -39,44 +37,23 @@ Source: Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY Destination: - cockroach: - Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST - Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT - Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE - MaxOpenConns: 0 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS - MaxIdleConns: 0 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME - EventPushConnRatio: 0.01 # ZITADEL_DATABASE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.5 # ZITADEL_DATABASE_COCKROACH_PROJECTIONSPOOLERCONNRATIO - Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS - User: - Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD - SSL: - Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY - # Postgres is used as soon as a value is set - # The values describe the possible fields to set values postgres: - Host: # ZITADEL_DATABASE_POSTGRES_HOST - Port: # ZITADEL_DATABASE_POSTGRES_PORT - Database: # ZITADEL_DATABASE_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS + Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST + Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT + Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE + MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS + MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS User: - Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME - Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD + Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD SSL: - Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE - RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT - Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT - Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY EventBulkSize: 10000 diff --git a/cmd/mirror/event.go b/cmd/mirror/event.go index 2bb0d52f45..d513990e10 100644 --- a/cmd/mirror/event.go +++ b/cmd/mirror/event.go @@ -4,7 +4,6 @@ import ( "context" "github.com/zitadel/zitadel/internal/v2/eventstore" - "github.com/zitadel/zitadel/internal/v2/projection" "github.com/zitadel/zitadel/internal/v2/readmodel" "github.com/zitadel/zitadel/internal/v2/system" mirror_event "github.com/zitadel/zitadel/internal/v2/system/mirror" @@ -30,39 +29,6 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore return lastSuccess, nil } -func writeMigrationStart(ctx context.Context, sourceES *eventstore.EventStore, id string, destination string) (_ float64, err error) { - var cmd *eventstore.Command - if len(instanceIDs) > 0 { - cmd, err = mirror_event.NewStartedInstancesCommand(destination, instanceIDs) - if err != nil { - return 0, err - } - } else { - cmd = mirror_event.NewStartedSystemCommand(destination) - } - - var position projection.HighestPosition - - err = sourceES.Push( - ctx, - eventstore.NewPushIntent( - system.AggregateInstance, - eventstore.AppendAggregate( - system.AggregateOwner, - system.AggregateType, - id, - eventstore.CurrentSequenceMatches(0), - eventstore.AppendCommands(cmd), - ), - eventstore.PushReducer(&position), - ), - ) - if err != nil { - return 0, err - } - return position.Position, nil -} - func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error { return destinationES.Push( ctx, diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 3825462126..8ce53b150a 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/logging" db "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/v2/database" "github.com/zitadel/zitadel/internal/v2/eventstore" @@ -57,9 +58,9 @@ func copyEventstore(ctx context.Context, config *Migration) { func positionQuery(db *db.DB) string { switch db.Type() { - case "postgres": + case dialect.DatabaseTypePostgres: return "SELECT EXTRACT(EPOCH FROM clock_timestamp())" - case "cockroach": + case dialect.DatabaseTypeCockroach: return "SELECT cluster_logical_timestamp()" default: logging.WithFields("db_type", db.Type()).Fatal("database type not recognized") @@ -80,9 +81,6 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { destConn, err := dest.Conn(ctx) logging.OnError(err).Fatal("unable to acquire dest connection") - sourceES := eventstore.NewEventstoreFromOne(postgres.New(source, &postgres.Config{ - MaxRetries: 3, - })) destinationES := eventstore.NewEventstoreFromOne(postgres.New(dest, &postgres.Config{ MaxRetries: 3, })) @@ -90,8 +88,14 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName()) logging.OnError(err).Fatal("unable to query latest successful migration") - maxPosition, err := writeMigrationStart(ctx, sourceES, migrationID, dest.DatabaseName()) - logging.OnError(err).Fatal("unable to write migration started event") + var maxPosition float64 + err = source.QueryRowContext(ctx, + func(row *sql.Row) error { + return row.Scan(&maxPosition) + }, + "SELECT MAX(position) FROM eventstore.events2 "+instanceClause(), + ) + logging.OnError(err).Fatal("unable to query max position from source") logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration") diff --git a/cmd/mirror/mirror.go b/cmd/mirror/mirror.go index 3fbfe1ae94..866238e56e 100644 --- a/cmd/mirror/mirror.go +++ b/cmd/mirror/mirror.go @@ -56,7 +56,6 @@ Order of execution: copyEventstore(cmd.Context(), config) projections(cmd.Context(), projectionConfig, masterKey) - verifyMigration(cmd.Context(), config) }, } diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index a4987a48f6..66b3fb1a26 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -84,6 +84,7 @@ type ProjectionsConfig struct { ExternalDomain string ExternalSecure bool InternalAuthZ internal_authz.Config + SystemAuthZ internal_authz.Config SystemDefaults systemdefaults.SystemDefaults Telemetry *handlers.TelemetryPusherConfig Login login.Config @@ -117,8 +118,11 @@ func projections( staticStorage, err := config.AssetStorage.NewStorage(client.DB) logging.OnError(err).Fatal("unable create static storage") - config.Eventstore.Querier = old_es.NewCRDB(client) - config.Eventstore.Pusher = new_es.NewEventstore(client) + newEventstore := new_es.NewEventstore(client) + config.Eventstore.Querier = old_es.NewPostgres(client) + config.Eventstore.Pusher = newEventstore + config.Eventstore.Searcher = newEventstore + es := eventstore.NewEventstore(config.Eventstore) esV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(client, &es_v4_pg.Config{ MaxRetries: config.Eventstore.MaxRetries, @@ -147,7 +151,7 @@ func projections( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, @@ -184,7 +188,7 @@ func projections( keys.Target, &http.Client{}, func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) }, sessionTokenVerifier, config.OIDC.DefaultAccessTokenLifetime, @@ -220,7 +224,7 @@ func projections( keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, - client, + nil, ) config.Auth.Spooler.Client = client @@ -247,7 +251,7 @@ func projections( } }() - for i := 0; i < int(config.Projections.ConcurrentInstances); i++ { + for range int(config.Projections.ConcurrentInstances) { go execProjections(ctx, instances, failedInstances, &wg) } @@ -269,31 +273,39 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc err := projection.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger failed") + failedInstances <- instance + continue + } + + err = projection.ProjectInstanceFields(ctx) + if err != nil { + logging.WithFields("instance", instance).WithError(err).Info("trigger fields failed") failedInstances <- instance continue } err = admin_handler.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger admin handler failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger admin handler failed") failedInstances <- instance continue } err = auth_handler.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger auth handler failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed") failedInstances <- instance continue } err = notification.ProjectInstance(ctx) if err != nil { - logging.WithFields("instance", instance).OnError(err).Info("trigger notification failed") + logging.WithFields("instance", instance).WithError(err).Info("trigger notification failed") failedInstances <- instance continue } + logging.WithFields("instance", instance).Info("projections done") } wg.Done() diff --git a/cmd/setup/07.go b/cmd/setup/07.go index 73b9d3480b..590b220eb3 100644 --- a/cmd/setup/07.go +++ b/cmd/setup/07.go @@ -3,7 +3,7 @@ package setup import ( "context" "database/sql" - "embed" + _ "embed" "strings" "github.com/zitadel/zitadel/internal/eventstore" @@ -12,31 +12,20 @@ import ( var ( //go:embed 07/logstore.sql createLogstoreSchema07 string - //go:embed 07/cockroach/access.sql - //go:embed 07/postgres/access.sql - createAccessLogsTable07 embed.FS - //go:embed 07/cockroach/execution.sql - //go:embed 07/postgres/execution.sql - createExecutionLogsTable07 embed.FS + //go:embed 07/access.sql + createAccessLogsTable07 string + //go:embed 07/execution.sql + createExecutionLogsTable07 string ) type LogstoreTables struct { dbClient *sql.DB username string - dbType string } func (mig *LogstoreTables) Execute(ctx context.Context, _ eventstore.Event) error { - accessStmt, err := readStmt(createAccessLogsTable07, "07", mig.dbType, "access.sql") - if err != nil { - return err - } - executionStmt, err := readStmt(createExecutionLogsTable07, "07", mig.dbType, "execution.sql") - if err != nil { - return err - } - stmt := strings.ReplaceAll(createLogstoreSchema07, "%[1]s", mig.username) + accessStmt + executionStmt - _, err = mig.dbClient.ExecContext(ctx, stmt) + stmt := strings.ReplaceAll(createLogstoreSchema07, "%[1]s", mig.username) + createAccessLogsTable07 + createExecutionLogsTable07 + _, err := mig.dbClient.ExecContext(ctx, stmt) return err } diff --git a/cmd/setup/07/postgres/access.sql b/cmd/setup/07/access.sql similarity index 100% rename from cmd/setup/07/postgres/access.sql rename to cmd/setup/07/access.sql diff --git a/cmd/setup/07/cockroach/access.sql b/cmd/setup/07/cockroach/access.sql deleted file mode 100644 index fc5354cf32..0000000000 --- a/cmd/setup/07/cockroach/access.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS logstore.access ( - log_date TIMESTAMPTZ NOT NULL - , protocol INT NOT NULL - , request_url TEXT NOT NULL - , response_status INT NOT NULL - , request_headers JSONB - , response_headers JSONB - , instance_id TEXT NOT NULL - , project_id TEXT NOT NULL - , requested_domain TEXT - , requested_host TEXT - - , INDEX protocol_date_desc (instance_id, protocol, log_date DESC) STORING (request_url, response_status, request_headers) -); diff --git a/cmd/setup/07/cockroach/execution.sql b/cmd/setup/07/cockroach/execution.sql deleted file mode 100644 index b8e18b525a..0000000000 --- a/cmd/setup/07/cockroach/execution.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS logstore.execution ( - log_date TIMESTAMPTZ NOT NULL - , took INTERVAL - , message TEXT NOT NULL - , loglevel INT NOT NULL - , instance_id TEXT NOT NULL - , action_id TEXT NOT NULL - , metadata JSONB - - , INDEX log_date_desc (instance_id, log_date DESC) STORING (took) -); diff --git a/cmd/setup/07/postgres/execution.sql b/cmd/setup/07/execution.sql similarity index 100% rename from cmd/setup/07/postgres/execution.sql rename to cmd/setup/07/execution.sql diff --git a/cmd/setup/08.go b/cmd/setup/08.go index bec6a65ebb..fa006bd3cf 100644 --- a/cmd/setup/08.go +++ b/cmd/setup/08.go @@ -2,16 +2,15 @@ package setup import ( "context" - "embed" + _ "embed" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" ) var ( - //go:embed 08/cockroach/08.sql - //go:embed 08/postgres/08.sql - tokenIndexes08 embed.FS + //go:embed 08/08.sql + tokenIndexes08 string ) type AuthTokenIndexes struct { @@ -19,11 +18,7 @@ type AuthTokenIndexes struct { } func (mig *AuthTokenIndexes) Execute(ctx context.Context, _ eventstore.Event) error { - stmt, err := readStmt(tokenIndexes08, "08", mig.dbClient.Type(), "08.sql") - if err != nil { - return err - } - _, err = mig.dbClient.ExecContext(ctx, stmt) + _, err := mig.dbClient.ExecContext(ctx, tokenIndexes08) return err } diff --git a/cmd/setup/08/postgres/08.sql b/cmd/setup/08/08.sql similarity index 100% rename from cmd/setup/08/postgres/08.sql rename to cmd/setup/08/08.sql diff --git a/cmd/setup/08/cockroach/08.sql b/cmd/setup/08/cockroach/08.sql deleted file mode 100644 index aec4d54303..0000000000 --- a/cmd/setup/08/cockroach/08.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE INDEX IF NOT EXISTS inst_refresh_tkn_idx ON auth.tokens(instance_id, refresh_token_id); -CREATE INDEX IF NOT EXISTS inst_app_tkn_idx ON auth.tokens(instance_id, application_id); -CREATE INDEX IF NOT EXISTS inst_ro_tkn_idx ON auth.tokens(instance_id, resource_owner); -DROP INDEX IF EXISTS auth.tokens@user_user_agent_idx; -CREATE INDEX IF NOT EXISTS inst_usr_agnt_tkn_idx ON auth.tokens(instance_id, user_id, user_agent_id); \ No newline at end of file diff --git a/cmd/setup/10.go b/cmd/setup/10.go index 93c017305c..b134fcab62 100644 --- a/cmd/setup/10.go +++ b/cmd/setup/10.go @@ -3,7 +3,7 @@ package setup import ( "context" "database/sql" - "embed" + _ "embed" "time" "github.com/cockroachdb/cockroach-go/v2/crdb" @@ -18,9 +18,8 @@ var ( correctCreationDate10CreateTable string //go:embed 10/10_fill_table.sql correctCreationDate10FillTable string - //go:embed 10/cockroach/10_update.sql - //go:embed 10/postgres/10_update.sql - correctCreationDate10Update embed.FS + //go:embed 10/10_update.sql + correctCreationDate10Update string //go:embed 10/10_count_wrong_events.sql correctCreationDate10CountWrongEvents string //go:embed 10/10_empty_table.sql @@ -40,11 +39,6 @@ func (mig *CorrectCreationDate) Execute(ctx context.Context, _ eventstore.Event) logging.WithFields("mig", mig.String(), "iteration", i).Debug("start iteration") var affected int64 err = crdb.ExecuteTx(ctx, mig.dbClient.DB, nil, func(tx *sql.Tx) error { - if mig.dbClient.Type() == "cockroach" { - if _, err := tx.Exec("SET experimental_enable_temp_tables=on"); err != nil { - return err - } - } _, err := tx.ExecContext(ctx, correctCreationDate10CreateTable) if err != nil { return err @@ -66,11 +60,7 @@ func (mig *CorrectCreationDate) Execute(ctx context.Context, _ eventstore.Event) return err } - updateStmt, err := readStmt(correctCreationDate10Update, "10", mig.dbClient.Type(), "10_update.sql") - if err != nil { - return err - } - _, err = tx.ExecContext(ctx, updateStmt) + _, err = tx.ExecContext(ctx, correctCreationDate10Update) if err != nil { return err } diff --git a/cmd/setup/10/postgres/10_update.sql b/cmd/setup/10/10_update.sql similarity index 100% rename from cmd/setup/10/postgres/10_update.sql rename to cmd/setup/10/10_update.sql diff --git a/cmd/setup/10/cockroach/10_update.sql b/cmd/setup/10/cockroach/10_update.sql deleted file mode 100644 index 9e7d7f993a..0000000000 --- a/cmd/setup/10/cockroach/10_update.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE eventstore.events e SET (creation_date, "position") = (we.next_cd, we.next_cd::DECIMAL) FROM wrong_events we WHERE e.event_sequence = we.event_sequence AND e.instance_id = we.instance_id; diff --git a/cmd/setup/14.go b/cmd/setup/14.go index f0ea1b819a..2cd5ac2c57 100644 --- a/cmd/setup/14.go +++ b/cmd/setup/14.go @@ -15,8 +15,7 @@ import ( ) var ( - //go:embed 14/cockroach/*.sql - //go:embed 14/postgres/*.sql + //go:embed 14/*.sql newEventsTable embed.FS ) @@ -40,7 +39,7 @@ func (mig *NewEventsTable) Execute(ctx context.Context, _ eventstore.Event) erro return err } - statements, err := readStatements(newEventsTable, "14", mig.dbClient.Type()) + statements, err := readStatements(newEventsTable, "14") if err != nil { return err } diff --git a/cmd/setup/14/cockroach/01_disable_inserts.sql b/cmd/setup/14/01_disable_inserts.sql similarity index 100% rename from cmd/setup/14/cockroach/01_disable_inserts.sql rename to cmd/setup/14/01_disable_inserts.sql diff --git a/cmd/setup/14/postgres/02_create_and_fill_events2.sql b/cmd/setup/14/02_create_and_fill_events2.sql similarity index 100% rename from cmd/setup/14/postgres/02_create_and_fill_events2.sql rename to cmd/setup/14/02_create_and_fill_events2.sql diff --git a/cmd/setup/14/postgres/03_events2_pk.sql b/cmd/setup/14/03_events2_pk.sql similarity index 100% rename from cmd/setup/14/postgres/03_events2_pk.sql rename to cmd/setup/14/03_events2_pk.sql diff --git a/cmd/setup/14/postgres/04_constraints.sql b/cmd/setup/14/04_constraints.sql similarity index 100% rename from cmd/setup/14/postgres/04_constraints.sql rename to cmd/setup/14/04_constraints.sql diff --git a/cmd/setup/14/postgres/05_indexes.sql b/cmd/setup/14/05_indexes.sql similarity index 100% rename from cmd/setup/14/postgres/05_indexes.sql rename to cmd/setup/14/05_indexes.sql diff --git a/cmd/setup/14/cockroach/02_create_and_fill_events2.sql b/cmd/setup/14/cockroach/02_create_and_fill_events2.sql deleted file mode 100644 index 300ac4b621..0000000000 --- a/cmd/setup/14/cockroach/02_create_and_fill_events2.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE eventstore.events2 ( - instance_id, - aggregate_type, - aggregate_id, - - event_type, - "sequence", - revision, - created_at, - payload, - creator, - "owner", - - "position", - in_tx_order, - - PRIMARY KEY (instance_id, aggregate_type, aggregate_id, "sequence") -) AS SELECT - instance_id, - aggregate_type, - aggregate_id, - - event_type, - event_sequence, - substr(aggregate_version, 2)::SMALLINT, - creation_date, - event_data, - editor_user, - resource_owner, - - creation_date::DECIMAL, - event_sequence -FROM eventstore.events_old; \ No newline at end of file diff --git a/cmd/setup/14/cockroach/03_constraints.sql b/cmd/setup/14/cockroach/03_constraints.sql deleted file mode 100644 index 62f119cc43..0000000000 --- a/cmd/setup/14/cockroach/03_constraints.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE eventstore.events2 ALTER COLUMN event_type SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN revision SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN creator SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN "owner" SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN "position" SET NOT NULL; -ALTER TABLE eventstore.events2 ALTER COLUMN in_tx_order SET NOT NULL; \ No newline at end of file diff --git a/cmd/setup/14/cockroach/04_indexes.sql b/cmd/setup/14/cockroach/04_indexes.sql deleted file mode 100644 index a442653606..0000000000 --- a/cmd/setup/14/cockroach/04_indexes.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX IF NOT EXISTS es_active_instances ON eventstore.events2 (created_at DESC) STORING ("position"); -CREATE INDEX IF NOT EXISTS es_wm ON eventstore.events2 (aggregate_id, instance_id, aggregate_type, event_type); -CREATE INDEX IF NOT EXISTS es_projection ON eventstore.events2 (instance_id, aggregate_type, event_type, "position"); \ No newline at end of file diff --git a/cmd/setup/14/postgres/01_disable_inserts.sql b/cmd/setup/14/postgres/01_disable_inserts.sql deleted file mode 100644 index 0f3c277eba..0000000000 --- a/cmd/setup/14/postgres/01_disable_inserts.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE eventstore.events RENAME TO events_old; \ No newline at end of file diff --git a/cmd/setup/15.go b/cmd/setup/15.go index 2e75ffb118..54161ddef9 100644 --- a/cmd/setup/15.go +++ b/cmd/setup/15.go @@ -11,8 +11,7 @@ import ( ) var ( - //go:embed 15/cockroach/*.sql - //go:embed 15/postgres/*.sql + //go:embed 15/*.sql currentProjectionState embed.FS ) @@ -21,7 +20,7 @@ type CurrentProjectionState struct { } func (mig *CurrentProjectionState) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(currentProjectionState, "15", mig.dbClient.Type()) + statements, err := readStatements(currentProjectionState, "15") if err != nil { return err } diff --git a/cmd/setup/15/cockroach/01_new_failed_events.sql b/cmd/setup/15/01_new_failed_events.sql similarity index 100% rename from cmd/setup/15/cockroach/01_new_failed_events.sql rename to cmd/setup/15/01_new_failed_events.sql diff --git a/cmd/setup/15/postgres/02_fe_from_projections.sql b/cmd/setup/15/02_fe_from_projections.sql similarity index 100% rename from cmd/setup/15/postgres/02_fe_from_projections.sql rename to cmd/setup/15/02_fe_from_projections.sql diff --git a/cmd/setup/15/cockroach/03_fe_from_adminapi.sql b/cmd/setup/15/03_fe_from_adminapi.sql similarity index 100% rename from cmd/setup/15/cockroach/03_fe_from_adminapi.sql rename to cmd/setup/15/03_fe_from_adminapi.sql diff --git a/cmd/setup/15/cockroach/04_fe_from_auth.sql b/cmd/setup/15/04_fe_from_auth.sql similarity index 100% rename from cmd/setup/15/cockroach/04_fe_from_auth.sql rename to cmd/setup/15/04_fe_from_auth.sql diff --git a/cmd/setup/15/cockroach/05_current_states.sql b/cmd/setup/15/05_current_states.sql similarity index 100% rename from cmd/setup/15/cockroach/05_current_states.sql rename to cmd/setup/15/05_current_states.sql diff --git a/cmd/setup/15/postgres/06_cs_from_projections.sql b/cmd/setup/15/06_cs_from_projections.sql similarity index 100% rename from cmd/setup/15/postgres/06_cs_from_projections.sql rename to cmd/setup/15/06_cs_from_projections.sql diff --git a/cmd/setup/15/postgres/07_cs_from_adminapi.sql b/cmd/setup/15/07_cs_from_adminapi.sql similarity index 100% rename from cmd/setup/15/postgres/07_cs_from_adminapi.sql rename to cmd/setup/15/07_cs_from_adminapi.sql diff --git a/cmd/setup/15/postgres/08_cs_from_auth.sql b/cmd/setup/15/08_cs_from_auth.sql similarity index 100% rename from cmd/setup/15/postgres/08_cs_from_auth.sql rename to cmd/setup/15/08_cs_from_auth.sql diff --git a/cmd/setup/15/cockroach/02_fe_from_projections.sql b/cmd/setup/15/cockroach/02_fe_from_projections.sql deleted file mode 100644 index 8bf7a4b8d4..0000000000 --- a/cmd/setup/15/cockroach/02_fe_from_projections.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.projection_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.error - , fe.last_failed -FROM - projections.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/06_cs_from_projections.sql b/cmd/setup/15/cockroach/06_cs_from_projections.sql deleted file mode 100644 index 579afb6d4c..0000000000 --- a/cmd/setup/15/cockroach/06_cs_from_projections.sql +++ /dev/null @@ -1,29 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.projection_name - , cs.instance_id - , e.created_at - , e.position - , cs.timestamp -FROM - projections.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.aggregate_type = cs.aggregate_type - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - projections.current_sequences cs2 - WHERE - cs.projection_name = cs2.projection_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/07_cs_from_adminapi.sql b/cmd/setup/15/cockroach/07_cs_from_adminapi.sql deleted file mode 100644 index c40d13a067..0000000000 --- a/cmd/setup/15/cockroach/07_cs_from_adminapi.sql +++ /dev/null @@ -1,28 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.view_name - , cs.instance_id - , e.created_at - , e.position - , cs.last_successful_spooler_run -FROM - adminapi.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - adminapi.current_sequences cs2 - WHERE - cs.view_name = cs2.view_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/cockroach/08_cs_from_auth.sql b/cmd/setup/15/cockroach/08_cs_from_auth.sql deleted file mode 100644 index c8e7236107..0000000000 --- a/cmd/setup/15/cockroach/08_cs_from_auth.sql +++ /dev/null @@ -1,28 +0,0 @@ -INSERT INTO projections.current_states ( - projection_name - , instance_id - , event_date - , "position" - , last_updated -) (SELECT - cs.view_name - , cs.instance_id - , e.created_at - , e.position - , cs.last_successful_spooler_run -FROM - auth.current_sequences cs -JOIN eventstore.events2 e ON - e.instance_id = cs.instance_id - AND e.sequence = cs.current_sequence - AND cs.current_sequence = ( - SELECT - MAX(cs2.current_sequence) - FROM - auth.current_sequences cs2 - WHERE - cs.view_name = cs2.view_name - AND cs.instance_id = cs2.instance_id - ) -) -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/01_new_failed_events.sql b/cmd/setup/15/postgres/01_new_failed_events.sql deleted file mode 100644 index 5fa39c08a5..0000000000 --- a/cmd/setup/15/postgres/01_new_failed_events.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS projections.failed_events2 ( - projection_name TEXT NOT NULL - , instance_id TEXT NOT NULL - - , aggregate_type TEXT NOT NULL - , aggregate_id TEXT NOT NULL - , event_creation_date TIMESTAMPTZ NOT NULL - , failed_sequence INT8 NOT NULL - - , failure_count INT2 NULL DEFAULT 0 - , error TEXT - , last_failed TIMESTAMPTZ - - , PRIMARY KEY (projection_name, instance_id, aggregate_type, aggregate_id, failed_sequence) -); -CREATE INDEX IF NOT EXISTS fe2_instance_id_idx on projections.failed_events2 (instance_id); \ No newline at end of file diff --git a/cmd/setup/15/postgres/03_fe_from_adminapi.sql b/cmd/setup/15/postgres/03_fe_from_adminapi.sql deleted file mode 100644 index 1616662fed..0000000000 --- a/cmd/setup/15/postgres/03_fe_from_adminapi.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.view_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.err_msg - , fe.last_failed -FROM - adminapi.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/04_fe_from_auth.sql b/cmd/setup/15/postgres/04_fe_from_auth.sql deleted file mode 100644 index a249293e24..0000000000 --- a/cmd/setup/15/postgres/04_fe_from_auth.sql +++ /dev/null @@ -1,26 +0,0 @@ -INSERT INTO projections.failed_events2 ( - projection_name - , instance_id - , aggregate_type - , aggregate_id - , event_creation_date - , failed_sequence - , failure_count - , error - , last_failed -) SELECT - fe.view_name - , fe.instance_id - , e.aggregate_type - , e.aggregate_id - , e.created_at - , e.sequence - , fe.failure_count - , fe.err_msg - , fe.last_failed -FROM - auth.failed_events fe -JOIN eventstore.events2 e ON - e.instance_id = fe.instance_id - AND e.sequence = fe.failed_sequence -ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/cmd/setup/15/postgres/05_current_states.sql b/cmd/setup/15/postgres/05_current_states.sql deleted file mode 100644 index bc2f5ed771..0000000000 --- a/cmd/setup/15/postgres/05_current_states.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE IF NOT EXISTS projections.current_states ( - projection_name TEXT NOT NULL - , instance_id TEXT NOT NULL - - , last_updated TIMESTAMPTZ - - , aggregate_id TEXT - , aggregate_type TEXT - , "sequence" INT8 - , event_date TIMESTAMPTZ - , "position" DECIMAL - - , PRIMARY KEY (projection_name, instance_id) -); -CREATE INDEX IF NOT EXISTS cs_instance_id_idx ON projections.current_states (instance_id); \ No newline at end of file diff --git a/cmd/setup/34.go b/cmd/setup/34.go index 59854e9e97..75e4076803 100644 --- a/cmd/setup/34.go +++ b/cmd/setup/34.go @@ -3,17 +3,14 @@ package setup import ( "context" _ "embed" - "fmt" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" ) var ( - //go:embed 34/cockroach/34_cache_schema.sql - addCacheSchemaCockroach string - //go:embed 34/postgres/34_cache_schema.sql - addCacheSchemaPostgres string + //go:embed 34/34_cache_schema.sql + addCacheSchema string ) type AddCacheSchema struct { @@ -21,14 +18,7 @@ type AddCacheSchema struct { } func (mig *AddCacheSchema) Execute(ctx context.Context, _ eventstore.Event) (err error) { - switch mig.dbClient.Type() { - case "cockroach": - _, err = mig.dbClient.ExecContext(ctx, addCacheSchemaCockroach) - case "postgres": - _, err = mig.dbClient.ExecContext(ctx, addCacheSchemaPostgres) - default: - err = fmt.Errorf("add cache schema: unsupported db type %q", mig.dbClient.Type()) - } + _, err = mig.dbClient.ExecContext(ctx, addCacheSchema) return err } diff --git a/cmd/setup/34/postgres/34_cache_schema.sql b/cmd/setup/34/34_cache_schema.sql similarity index 100% rename from cmd/setup/34/postgres/34_cache_schema.sql rename to cmd/setup/34/34_cache_schema.sql diff --git a/cmd/setup/34/cockroach/34_cache_schema.sql b/cmd/setup/34/cockroach/34_cache_schema.sql deleted file mode 100644 index 0f866b0ccd..0000000000 --- a/cmd/setup/34/cockroach/34_cache_schema.sql +++ /dev/null @@ -1,27 +0,0 @@ -create schema if not exists cache; - -create table if not exists cache.objects ( - cache_name varchar not null, - id uuid not null default gen_random_uuid(), - created_at timestamptz not null default now(), - last_used_at timestamptz not null default now(), - payload jsonb not null, - - primary key(cache_name, id) -); - -create table if not exists cache.string_keys( - cache_name varchar not null check (cache_name <> ''), - index_id integer not null check (index_id > 0), - index_key varchar not null check (index_key <> ''), - object_id uuid not null, - - primary key (cache_name, index_id, index_key), - constraint fk_object - foreign key(cache_name, object_id) - references cache.objects(cache_name, id) - on delete cascade -); - -create index if not exists string_keys_object_id_idx - on cache.string_keys (cache_name, object_id); -- for delete cascade diff --git a/cmd/setup/35.go b/cmd/setup/35.go index f8473cfbfd..68e08bdfdb 100644 --- a/cmd/setup/35.go +++ b/cmd/setup/35.go @@ -21,7 +21,7 @@ type AddPositionToIndexEsWm struct { } func (mig *AddPositionToIndexEsWm) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(addPositionToEsWmIndex, "35", "") + statements, err := readStatements(addPositionToEsWmIndex, "35") if err != nil { return err } diff --git a/cmd/setup/40.go b/cmd/setup/40.go index b16b9226f7..86cdab0d11 100644 --- a/cmd/setup/40.go +++ b/cmd/setup/40.go @@ -24,8 +24,7 @@ const ( ) var ( - //go:embed 40/cockroach/*.sql - //go:embed 40/postgres/*.sql + //go:embed 40/*.sql initPushFunc embed.FS ) @@ -112,5 +111,5 @@ func (mig *InitPushFunc) inTxOrderType(ctx context.Context) (typeName string, er } func (mig *InitPushFunc) filePath(fileName string) string { - return path.Join("40", mig.dbClient.Type(), fileName) + return path.Join("40", fileName) } diff --git a/cmd/setup/40/cockroach/00_in_tx_order_type.sql b/cmd/setup/40/00_in_tx_order_type.sql similarity index 100% rename from cmd/setup/40/cockroach/00_in_tx_order_type.sql rename to cmd/setup/40/00_in_tx_order_type.sql diff --git a/cmd/setup/40/postgres/01_type.sql b/cmd/setup/40/01_type.sql similarity index 100% rename from cmd/setup/40/postgres/01_type.sql rename to cmd/setup/40/01_type.sql diff --git a/cmd/setup/40/postgres/02_func.sql b/cmd/setup/40/02_func.sql similarity index 100% rename from cmd/setup/40/postgres/02_func.sql rename to cmd/setup/40/02_func.sql diff --git a/cmd/setup/40/cockroach/01_type.sql b/cmd/setup/40/cockroach/01_type.sql deleted file mode 100644 index e26af2f828..0000000000 --- a/cmd/setup/40/cockroach/01_type.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TYPE IF NOT EXISTS eventstore.command AS ( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - , command_type TEXT - , revision INT2 - , payload JSONB - , creator TEXT - , owner TEXT -); diff --git a/cmd/setup/40/cockroach/02_func.sql b/cmd/setup/40/cockroach/02_func.sql deleted file mode 100644 index 9cb45529ad..0000000000 --- a/cmd/setup/40/cockroach/02_func.sql +++ /dev/null @@ -1,137 +0,0 @@ -CREATE OR REPLACE FUNCTION eventstore.latest_aggregate_state( - instance_id TEXT - , aggregate_type TEXT - , aggregate_id TEXT - - , sequence OUT BIGINT - , owner OUT TEXT -) - LANGUAGE 'plpgsql' -AS $$ - BEGIN - SELECT - COALESCE(e.sequence, 0) AS sequence - , e.owner - INTO - sequence - , owner - FROM - eventstore.events2 e - WHERE - e.instance_id = $1 - AND e.aggregate_type = $2 - AND e.aggregate_id = $3 - ORDER BY - e.sequence DESC - LIMIT 1; - - RETURN; - END; -$$; - -CREATE OR REPLACE FUNCTION eventstore.commands_to_events2(commands eventstore.command[]) - RETURNS eventstore.events2[] - LANGUAGE 'plpgsql' -AS $$ -DECLARE - current_sequence BIGINT; - current_owner TEXT; - - instance_id TEXT; - aggregate_type TEXT; - aggregate_id TEXT; - - _events eventstore.events2[]; - - _aggregates CURSOR FOR - select - DISTINCT ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - FROM - UNNEST(commands) AS c; -BEGIN - OPEN _aggregates; - LOOP - FETCH NEXT IN _aggregates INTO instance_id, aggregate_type, aggregate_id; - -- crdb does not support EXIT WHEN NOT FOUND - EXIT WHEN instance_id IS NULL; - - SELECT - * - INTO - current_sequence - , current_owner - FROM eventstore.latest_aggregate_state( - instance_id - , aggregate_type - , aggregate_id - ); - - -- RETURN QUERY is not supported by crdb: https://github.com/cockroachdb/cockroach/issues/105240 - SELECT - ARRAY_CAT(_events, ARRAY_AGG(e)) - INTO - _events - FROM ( - SELECT - ("c").instance_id - , ("c").aggregate_type - , ("c").aggregate_id - , ("c").command_type -- AS event_type - , COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence - , ("c").revision - , NOW() -- AS created_at - , ("c").payload - , ("c").creator - , COALESCE(current_owner, ("c").owner) -- AS owner - , cluster_logical_timestamp() -- AS position - , ordinality::{{ .InTxOrderType }} -- AS in_tx_order - FROM - UNNEST(commands) WITH ORDINALITY AS c - WHERE - ("c").instance_id = instance_id - AND ("c").aggregate_type = aggregate_type - AND ("c").aggregate_id = aggregate_id - ) AS e; - END LOOP; - CLOSE _aggregates; - RETURN _events; -END; -$$; - -CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 AS $$ - INSERT INTO eventstore.events2 - SELECT - ("e").instance_id - , ("e").aggregate_type - , ("e").aggregate_id - , ("e").event_type - , ("e").sequence - , ("e").revision - , ("e").created_at - , ("e").payload - , ("e").creator - , ("e").owner - , ("e")."position" - , ("e").in_tx_order - FROM - UNNEST(eventstore.commands_to_events2(commands)) e - ORDER BY - in_tx_order - RETURNING * -$$ LANGUAGE SQL; - -/* -select (c).* from UNNEST(eventstore.commands_to_events2( -ARRAY[ - ROW('', 'system', 'SYSTEM', 'ct1', 1, '{"key": "value"}', 'c1', 'SYSTEM') - , ROW('', 'system', 'SYSTEM', 'ct2', 1, '{"key": "value"}', 'c1', 'SYSTEM') - , ROW('289525561255060732', 'org', '289575074711790844', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('289525561255060732', 'user', '289575075164906748', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('289525561255060732', 'oidc_session', 'V2_289575178579535100', 'ct3', 1, '{"key": "value"}', 'c1', '289575074711790844') - , ROW('', 'system', 'SYSTEM', 'ct3', 1, '{"key": "value"}', 'c1', 'SYSTEM') -]::eventstore.command[] -) )c; -*/ - diff --git a/cmd/setup/40/postgres/00_in_tx_order_type.sql b/cmd/setup/40/postgres/00_in_tx_order_type.sql deleted file mode 100644 index 68b7daf984..0000000000 --- a/cmd/setup/40/postgres/00_in_tx_order_type.sql +++ /dev/null @@ -1,5 +0,0 @@ -SELECT data_type -FROM information_schema.columns -WHERE table_schema = 'eventstore' -AND table_name = 'events2' -AND column_name = 'in_tx_order'; diff --git a/cmd/setup/43.go b/cmd/setup/43.go index 844c25cf24..1fa09773bc 100644 --- a/cmd/setup/43.go +++ b/cmd/setup/43.go @@ -12,8 +12,7 @@ import ( ) var ( - //go:embed 43/cockroach/*.sql - //go:embed 43/postgres/*.sql + //go:embed 43/*.sql createFieldsDomainIndex embed.FS ) @@ -22,7 +21,7 @@ type CreateFieldsDomainIndex struct { } func (mig *CreateFieldsDomainIndex) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(createFieldsDomainIndex, "43", mig.dbClient.Type()) + statements, err := readStatements(createFieldsDomainIndex, "43") if err != nil { return err } diff --git a/cmd/setup/43/postgres/43.sql b/cmd/setup/43/43.sql similarity index 100% rename from cmd/setup/43/postgres/43.sql rename to cmd/setup/43/43.sql diff --git a/cmd/setup/43/cockroach/43.sql b/cmd/setup/43/cockroach/43.sql deleted file mode 100644 index 9152130970..0000000000 --- a/cmd/setup/43/cockroach/43.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX CONCURRENTLY IF NOT EXISTS fields_instance_domains_idx -ON eventstore.fields (object_id) -WHERE object_type = 'instance_domain' AND field_name = 'domain'; \ No newline at end of file diff --git a/cmd/setup/44.go b/cmd/setup/44.go index 11c355a053..5eb2f8d5c1 100644 --- a/cmd/setup/44.go +++ b/cmd/setup/44.go @@ -21,7 +21,7 @@ type ReplaceCurrentSequencesIndex struct { } func (mig *ReplaceCurrentSequencesIndex) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(replaceCurrentSequencesIndex, "44", "") + statements, err := readStatements(replaceCurrentSequencesIndex, "44") if err != nil { return err } diff --git a/cmd/setup/46.go b/cmd/setup/46.go index e48b16e4b0..3593a1b668 100644 --- a/cmd/setup/46.go +++ b/cmd/setup/46.go @@ -21,7 +21,7 @@ var ( ) func (mig *InitPermissionFunctions) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(permissionFunctions, "46", "") + statements, err := readStatements(permissionFunctions, "46") if err != nil { return err } diff --git a/cmd/setup/48.go b/cmd/setup/48.go new file mode 100644 index 0000000000..2da0ad51a8 --- /dev/null +++ b/cmd/setup/48.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 48.sql + addSAMLAppLoginVersion string +) + +type Apps7SAMLConfigsLoginVersion struct { + dbClient *database.DB +} + +func (mig *Apps7SAMLConfigsLoginVersion) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, addSAMLAppLoginVersion) + return err +} + +func (mig *Apps7SAMLConfigsLoginVersion) String() string { + return "48_apps7_saml_configs_login_version" +} diff --git a/cmd/setup/48.sql b/cmd/setup/48.sql new file mode 100644 index 0000000000..018231f59e --- /dev/null +++ b/cmd/setup/48.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS projections.apps7_saml_configs ADD COLUMN IF NOT EXISTS login_version SMALLINT; +ALTER TABLE IF EXISTS projections.apps7_saml_configs ADD COLUMN IF NOT EXISTS login_base_uri TEXT; diff --git a/cmd/setup/49.go b/cmd/setup/49.go new file mode 100644 index 0000000000..8465589140 --- /dev/null +++ b/cmd/setup/49.go @@ -0,0 +1,39 @@ +package setup + +import ( + "context" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +type InitPermittedOrgsFunction struct { + eventstoreClient *database.DB +} + +var ( + //go:embed 49/*.sql + permittedOrgsFunction embed.FS +) + +func (mig *InitPermittedOrgsFunction) Execute(ctx context.Context, _ eventstore.Event) error { + statements, err := readStatements(permittedOrgsFunction, "49") + if err != nil { + return err + } + for _, stmt := range statements { + logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") + if _, err := mig.eventstoreClient.ExecContext(ctx, stmt.query); err != nil { + return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) + } + } + return nil +} + +func (*InitPermittedOrgsFunction) String() string { + return "49_init_permitted_orgs_function" +} diff --git a/cmd/setup/49/01-permitted_orgs_function.sql b/cmd/setup/49/01-permitted_orgs_function.sql new file mode 100644 index 0000000000..84ee2e5f2d --- /dev/null +++ b/cmd/setup/49/01-permitted_orgs_function.sql @@ -0,0 +1,56 @@ +DROP FUNCTION IF EXISTS eventstore.permitted_orgs; + +CREATE OR REPLACE FUNCTION eventstore.permitted_orgs( + instanceId TEXT + , userId TEXT + , perm TEXT + , filter_orgs TEXT + + , org_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' + STABLE +AS $$ +DECLARE + matched_roles TEXT[]; -- roles containing permission +BEGIN + SELECT array_agg(rp.role) INTO matched_roles + FROM eventstore.role_permissions rp + WHERE rp.instance_id = instanceId + AND rp.permission = perm; + + -- First try if the permission was granted thru an instance-level role + DECLARE + has_instance_permission bool; + BEGIN + SELECT true INTO has_instance_permission + FROM eventstore.instance_members im + WHERE im.role = ANY(matched_roles) + AND im.instance_id = instanceId + AND im.user_id = userId + LIMIT 1; + + IF has_instance_permission THEN + -- Return all organizations or only those in filter_orgs + SELECT array_agg(o.org_id) INTO org_ids + FROM eventstore.instance_orgs o + WHERE o.instance_id = instanceId + AND CASE WHEN filter_orgs != '' + THEN o.org_id IN (filter_orgs) + ELSE TRUE END; + RETURN; + END IF; + END; + + -- Return the organizations where permission were granted thru org-level roles + SELECT array_agg(sub.org_id) INTO org_ids + FROM ( + SELECT DISTINCT om.org_id + FROM eventstore.org_members om + WHERE om.role = ANY(matched_roles) + AND om.instance_id = instanceID + AND om.user_id = userId + ) AS sub; + RETURN; +END; +$$; diff --git a/cmd/setup/50.go b/cmd/setup/50.go new file mode 100644 index 0000000000..fea69f79ce --- /dev/null +++ b/cmd/setup/50.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 50.sql + addUsePKCE string +) + +type IDPTemplate6UsePKCE struct { + dbClient *database.DB +} + +func (mig *IDPTemplate6UsePKCE) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, addUsePKCE) + return err +} + +func (mig *IDPTemplate6UsePKCE) String() string { + return "50_idp_templates6_add_use_pkce" +} diff --git a/cmd/setup/50.sql b/cmd/setup/50.sql new file mode 100644 index 0000000000..4ff0fd7042 --- /dev/null +++ b/cmd/setup/50.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS projections.idp_templates6_oauth2 ADD COLUMN IF NOT EXISTS use_pkce BOOLEAN; +ALTER TABLE IF EXISTS projections.idp_templates6_oidc ADD COLUMN IF NOT EXISTS use_pkce BOOLEAN; \ No newline at end of file diff --git a/cmd/setup/51.go b/cmd/setup/51.go new file mode 100644 index 0000000000..799daf744e --- /dev/null +++ b/cmd/setup/51.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 51.sql + addRootCA string +) + +type IDPTemplate6RootCA struct { + dbClient *database.DB +} + +func (mig *IDPTemplate6RootCA) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, addRootCA) + return err +} + +func (mig *IDPTemplate6RootCA) String() string { + return "51_idp_templates6_add_root_ca" +} diff --git a/cmd/setup/51.sql b/cmd/setup/51.sql new file mode 100644 index 0000000000..a4146219c4 --- /dev/null +++ b/cmd/setup/51.sql @@ -0,0 +1 @@ +ALTER TABLE IF EXISTS projections.idp_templates6_ldap2 ADD COLUMN IF NOT EXISTS root_ca BYTEA; \ No newline at end of file diff --git a/cmd/setup/52.go b/cmd/setup/52.go new file mode 100644 index 0000000000..f5fc238c93 --- /dev/null +++ b/cmd/setup/52.go @@ -0,0 +1,47 @@ +package setup + +import ( + "context" + "database/sql" + _ "embed" + "errors" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 52/alter.sql + renameTableIfNotExisting string + //go:embed 52/check.sql + checkIfTableIsExisting string +) + +type IDPTemplate6LDAP2 struct { + dbClient *database.DB +} + +func (mig *IDPTemplate6LDAP2) Execute(ctx context.Context, _ eventstore.Event) error { + var count int + err := mig.dbClient.QueryRowContext(ctx, + func(row *sql.Row) error { + if err := row.Scan(&count); err != nil { + return err + } + return row.Err() + }, + checkIfTableIsExisting, + ) + if err == nil { + return nil + } + if !errors.Is(err, sql.ErrNoRows) { + return err + } + _, err = mig.dbClient.ExecContext(ctx, renameTableIfNotExisting) + return err +} + +func (mig *IDPTemplate6LDAP2) String() string { + return "52_idp_templates6_ldap2" +} diff --git a/cmd/setup/52/alter.sql b/cmd/setup/52/alter.sql new file mode 100644 index 0000000000..66df66d4e9 --- /dev/null +++ b/cmd/setup/52/alter.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS projections.idp_templates6_ldap3 RENAME COLUMN rootCA TO root_ca; +ALTER TABLE IF EXISTS projections.idp_templates6_ldap3 RENAME TO idp_templates6_ldap2; \ No newline at end of file diff --git a/cmd/setup/52/check.sql b/cmd/setup/52/check.sql new file mode 100644 index 0000000000..f5eb07a341 --- /dev/null +++ b/cmd/setup/52/check.sql @@ -0,0 +1,4 @@ +SELECT 1 +FROM information_schema.tables +WHERE table_schema = 'projections' + AND table_name = 'idp_templates6_ldap2'; diff --git a/cmd/setup/53.go b/cmd/setup/53.go new file mode 100644 index 0000000000..952fc37916 --- /dev/null +++ b/cmd/setup/53.go @@ -0,0 +1,37 @@ +package setup + +import ( + "context" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +type InitPermittedOrgsFunction53 struct { + dbClient *database.DB +} + +//go:embed 53/*.sql +var permittedOrgsFunction53 embed.FS + +func (mig *InitPermittedOrgsFunction53) Execute(ctx context.Context, _ eventstore.Event) error { + statements, err := readStatements(permittedOrgsFunction53, "53") + if err != nil { + return err + } + for _, stmt := range statements { + logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") + if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil { + return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) + } + } + return nil +} + +func (*InitPermittedOrgsFunction53) String() string { + return "53_init_permitted_orgs_function_v2" +} diff --git a/cmd/setup/53/01-get-permissions-from-JSON.sql b/cmd/setup/53/01-get-permissions-from-JSON.sql new file mode 100644 index 0000000000..531184dbe4 --- /dev/null +++ b/cmd/setup/53/01-get-permissions-from-JSON.sql @@ -0,0 +1,114 @@ +DROP FUNCTION IF EXISTS eventstore.check_system_user_perms; +DROP FUNCTION IF EXISTS eventstore.get_system_permissions; +DROP TYPE IF EXISTS eventstore.project_grant; + + +/* + Function get_system_permissions unpacks an JSON array of system member permissions, + into a table format. Each array entry maps to one row representing a membership which + contained the req_permission. + + [ + { + "member_type": "IAM", + "aggregate_id": "310716990375453665", + "object_id": "", + "permissions": ["iam.read", "iam.write", "iam.policy.read"] + }, + ... + ] + + | member_type | aggregate_id | object_id | + | "IAM" | "310716990375453665" | null | +*/ +CREATE OR REPLACE FUNCTION eventstore.get_system_permissions( + permissions_json JSONB + , permm TEXT +) +RETURNS TABLE ( + member_type TEXT, + aggregate_id TEXT, + object_id TEXT +) + LANGUAGE 'plpgsql' IMMUTABLE +AS $$ +BEGIN + RETURN QUERY + SELECT res.member_type, res.aggregate_id, res.object_id FROM ( + SELECT + (perm)->>'member_type' AS member_type, + (perm)->>'aggregate_id' AS aggregate_id, + (perm)->>'object_id' AS object_id, + permission + FROM jsonb_array_elements(permissions_json) AS perm + CROSS JOIN jsonb_array_elements_text(perm->'permissions') AS permission) AS res + WHERE res.permission = permm; +END; +$$; + +/* + Type project_grant is composite identifier using its project and grant IDs. +*/ +CREATE TYPE eventstore.project_grant AS ( + project_id TEXT -- mapped from a permission's aggregate_id + , grant_id TEXT -- mapped from a permission's object_id +); + +/* + Function check_system_user_perms uses system member permissions to establish + on which organization, project or project grant the user has the requested permission. + The permission can also apply to the complete instance when a IAM membership matches + the requested instance ID, or through system membership. + + See eventstore.get_system_permissions() on the supported JSON format. +*/ +CREATE OR REPLACE FUNCTION eventstore.check_system_user_perms( + system_user_perms JSONB + , req_instance_id TEXT + , perm TEXT + + , instance_permitted OUT BOOLEAN + , org_ids OUT TEXT[] + , project_ids OUT TEXT[] + , project_grants OUT eventstore.project_grant[] +) + LANGUAGE 'plpgsql' IMMUTABLE +AS $$ +BEGIN + -- make sure no nulls are returned + instance_permitted := FALSE; + org_ids := ARRAY[]::TEXT[]; + project_ids := ARRAY[]::TEXT[]; + project_grants := ARRAY[]::eventstore.project_grant[]; + DECLARE + p RECORD; + BEGIN + FOR p IN SELECT member_type, aggregate_id, object_id + FROM eventstore.get_system_permissions(system_user_perms, perm) + LOOP + CASE p.member_type + WHEN 'System' THEN + instance_permitted := TRUE; + RETURN; + WHEN 'IAM' THEN + IF p.aggregate_id = req_instance_id THEN + instance_permitted := TRUE; + RETURN; + END IF; + WHEN 'Organization' THEN + IF p.aggregate_id != '' THEN + org_ids := array_append(org_ids, p.aggregate_id); + END IF; + WHEN 'Project' THEN + IF p.aggregate_id != '' THEN + project_ids := array_append(project_ids, p.aggregate_id); + END IF; + WHEN 'ProjectGrant' THEN + IF p.aggregate_id != '' THEN + project_grants := array_append(project_grants, ROW(p.aggregate_id, p.object_id)::eventstore.project_grant); + END IF; + END CASE; + END LOOP; + END; +END; +$$; diff --git a/cmd/setup/53/02-permitted_orgs_function.sql b/cmd/setup/53/02-permitted_orgs_function.sql new file mode 100644 index 0000000000..fbc7eaee59 --- /dev/null +++ b/cmd/setup/53/02-permitted_orgs_function.sql @@ -0,0 +1,71 @@ +DROP FUNCTION IF EXISTS eventstore.permitted_orgs; +DROP FUNCTION IF EXISTS eventstore.find_roles; + +-- find_roles finds all roles containing the permission +CREATE OR REPLACE FUNCTION eventstore.find_roles( + req_instance_id TEXT + , perm TEXT + + , roles OUT TEXT[] +) +LANGUAGE 'plpgsql' STABLE +AS $$ +BEGIN + SELECT array_agg(rp.role) INTO roles + FROM eventstore.role_permissions rp + WHERE rp.instance_id = req_instance_id + AND rp.permission = perm; +END; +$$; + +CREATE OR REPLACE FUNCTION eventstore.permitted_orgs( + req_instance_id TEXT + , auth_user_id TEXT + , system_user_perms JSONB + , perm TEXT + , filter_org TEXT + + , instance_permitted OUT BOOLEAN + , org_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' STABLE +AS $$ +BEGIN + -- if system user + IF system_user_perms IS NOT NULL THEN + SELECT p.instance_permitted, p.org_ids INTO instance_permitted, org_ids + FROM eventstore.check_system_user_perms(system_user_perms, req_instance_id, perm) p; + RETURN; + END IF; + + -- if human/machine user + DECLARE + matched_roles TEXT[] := eventstore.find_roles(req_instance_id, perm); + BEGIN + -- First try if the permission was granted thru an instance-level role + SELECT true INTO instance_permitted + FROM eventstore.instance_members im + WHERE im.role = ANY(matched_roles) + AND im.instance_id = req_instance_id + AND im.user_id = auth_user_id + LIMIT 1; + + org_ids := ARRAY[]::TEXT[]; + IF instance_permitted THEN + RETURN; + END IF; + instance_permitted := FALSE; + + -- Return the organizations where permission were granted thru org-level roles + SELECT array_agg(sub.org_id) INTO org_ids + FROM ( + SELECT DISTINCT om.org_id + FROM eventstore.org_members om + WHERE om.role = ANY(matched_roles) + AND om.instance_id = req_instance_id + AND om.user_id = auth_user_id + AND (filter_org IS NULL OR om.org_id = filter_org) + ) AS sub; + END; +END; +$$; diff --git a/cmd/setup/53/03-permitted_projects_func.sql b/cmd/setup/53/03-permitted_projects_func.sql new file mode 100644 index 0000000000..8c17481ce8 --- /dev/null +++ b/cmd/setup/53/03-permitted_projects_func.sql @@ -0,0 +1,58 @@ +-- recreate the view to include the resource_owner +CREATE OR REPLACE VIEW eventstore.project_members AS +SELECT instance_id, aggregate_id as project_id, object_id as user_id, text_value as role, resource_owner as org_id +FROM eventstore.fields +WHERE aggregate_type = 'project' +AND object_type = 'project_member_role' +AND field_name = 'project_role'; + +DROP FUNCTION IF EXISTS eventstore.permitted_projects; + +CREATE OR REPLACE FUNCTION eventstore.permitted_projects( + req_instance_id TEXT + , auth_user_id TEXT + , system_user_perms JSONB + , perm TEXT + , filter_org TEXT + + , instance_permitted OUT BOOLEAN + , org_ids OUT TEXT[] + , project_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' STABLE +AS $$ +BEGIN + -- if system user + IF system_user_perms IS NOT NULL THEN + SELECT p.instance_permitted, p.org_ids INTO instance_permitted, org_ids, project_ids + FROM eventstore.check_system_user_perms(system_user_perms, req_instance_id, perm) p; + RETURN; + END IF; + + -- if human/machine user + SELECT * FROM eventstore.permitted_orgs( + req_instance_id + , auth_user_id + , system_user_perms + , perm + , filter_org + ) INTO instance_permitted, org_ids; + IF instance_permitted THEN + RETURN; + END IF; + DECLARE + matched_roles TEXT[] := eventstore.find_roles(req_instance_id, perm); + BEGIN + -- Get the projects where permission were granted thru project-level roles + SELECT array_agg(sub.project_id) INTO project_ids + FROM ( + SELECT DISTINCT pm.project_id + FROM eventstore.project_members pm + WHERE pm.role = ANY(matched_roles) + AND pm.instance_id = req_instance_id + AND pm.user_id = auth_user_id + AND (filter_org IS NULL OR pm.org_id = filter_org) + ) AS sub; + END; +END; +$$; diff --git a/cmd/setup/cleanup.go b/cmd/setup/cleanup.go index 943ac164ea..69f7c72e53 100644 --- a/cmd/setup/cleanup.go +++ b/cmd/setup/cleanup.go @@ -21,21 +21,19 @@ func NewCleanup() *cobra.Command { Long: `cleans up migration if they got stuck`, Run: func(cmd *cobra.Command, args []string) { config := MustNewConfig(viper.GetViper()) - Cleanup(config) + Cleanup(cmd.Context(), config) }, } } -func Cleanup(config *Config) { - ctx := context.Background() - +func Cleanup(ctx context.Context, config *Config) { logging.Info("cleanup started") dbClient, err := database.Connect(config.Database, false) logging.OnError(err).Fatal("unable to connect to database") config.Eventstore.Pusher = new_es.NewEventstore(dbClient) - config.Eventstore.Querier = old_es.NewCRDB(dbClient) + config.Eventstore.Querier = old_es.NewPostgres(dbClient) es := eventstore.NewEventstore(config.Eventstore) step, err := migration.LastStuckStep(ctx, es) diff --git a/cmd/setup/config.go b/cmd/setup/config.go index f3215fd980..4742b94c7b 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -12,7 +12,7 @@ import ( "github.com/zitadel/zitadel/cmd/encryption" "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/cache/connector" @@ -22,10 +22,12 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/handlers" "github.com/zitadel/zitadel/internal/query/projection" static_config "github.com/zitadel/zitadel/internal/static/config" + metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config" ) type Config struct { @@ -33,16 +35,19 @@ type Config struct { Database database.Config Caches *connector.CachesConfig SystemDefaults systemdefaults.SystemDefaults - InternalAuthZ internal_authz.Config + InternalAuthZ authz.Config + SystemAuthZ authz.Config ExternalDomain string ExternalPort uint16 ExternalSecure bool Log *logging.Config + Metrics metrics.Config EncryptionKeys *encryption.EncryptionKeyConfig DefaultInstance command.InstanceSetup Machine *id.Config Projections projection.Config Notifications handlers.WorkerConfig + Executions execution.WorkerConfig Eventstore *eventstore.Config InitProjections InitProjections @@ -51,7 +56,7 @@ type Config struct { Login login.Config WebAuthNName string Telemetry *handlers.TelemetryPusherConfig - SystemAPIUsers map[string]*internal_authz.SystemAPIUser + SystemAPIUsers map[string]*authz.SystemAPIUser } type InitProjections struct { @@ -66,12 +71,12 @@ func MustNewConfig(v *viper.Viper) *Config { err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( hooks.SliceTypeStringDecode[*domain.CustomMessageText], - hooks.SliceTypeStringDecode[internal_authz.RoleMapping], - hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.SliceTypeStringDecode[authz.RoleMapping], + hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], hooks.MapHTTPHeaderStringDecode, - database.DecodeHook, + database.DecodeHook(false), actions.HTTPConfigDecodeHook, - hook.EnumHookFunc(internal_authz.MemberTypeString), + hook.EnumHookFunc(authz.MemberTypeString), hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), @@ -85,6 +90,9 @@ func MustNewConfig(v *viper.Viper) *Config { err = config.Log.SetLogger() logging.OnError(err).Fatal("unable to set logger") + err = config.Metrics.NewMeter() + logging.OnError(err).Fatal("unable to set meter") + id.Configure(config.Machine) // Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API. @@ -136,6 +144,12 @@ type Steps struct { s45CorrectProjectOwners *CorrectProjectOwners s46InitPermissionFunctions *InitPermissionFunctions s47FillMembershipFields *FillMembershipFields + s48Apps7SAMLConfigsLoginVersion *Apps7SAMLConfigsLoginVersion + s49InitPermittedOrgsFunction *InitPermittedOrgsFunction + s50IDPTemplate6UsePKCE *IDPTemplate6UsePKCE + s51IDPTemplate6RootCA *IDPTemplate6RootCA + s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2 + s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53 } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/integration_test/permission_test.go b/cmd/setup/integration_test/permission_test.go new file mode 100644 index 0000000000..2b0c56865f --- /dev/null +++ b/cmd/setup/integration_test/permission_test.go @@ -0,0 +1,871 @@ +//go:build integration + +package setup_test + +import ( + "encoding/json" + "testing" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/permission" + "github.com/zitadel/zitadel/internal/repository/project" +) + +func TestGetSystemPermissions(t *testing.T) { + const query = "SELECT * FROM eventstore.get_system_permissions($1, $2);" + t.Parallel() + permissions := []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project.read", "project.write"}, + }, + } + type result struct { + MemberType authz.MemberType + AggregateID string + ObjectID string + } + tests := []struct { + permm string + want []result + }{ + { + permm: "iam.read", + want: []result{ + { + MemberType: authz.MemberTypeSystem, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + }, + }, + }, + { + permm: "org.read", + want: []result{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + }, + }, + }, + { + permm: "project.write", + want: []result{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.permm, func(t *testing.T) { + t.Parallel() + rows, err := dbPool.Query(CTX, query, database.NewJSONArray(permissions), tt.permm) + require.NoError(t, err) + got, err := pgx.CollectRows(rows, pgx.RowToStructByPos[result]) + require.NoError(t, err) + assert.ElementsMatch(t, tt.want, got) + }) + } +} + +func TestCheckSystemUserPerms(t *testing.T) { + // Use JSON because of the composite project_grants SQL type + const query = "SELECT row_to_json(eventstore.check_system_user_perms($1, $2, $3));" + t.Parallel() + type args struct { + reqInstanceID string + permissions []authz.SystemUserPermissions + permm string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "iam.read, instance permitted from system", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project.read", "project.write"}, + }, + }, + permm: "iam.read", + }, + want: `{ + "instance_permitted": true, + "org_ids": [], + "project_grants": [], + "project_ids": [] + }`, + }, + { + name: "org.read, instance permitted", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project.read", "project.write"}, + }, + }, + permm: "org.read", + }, + want: `{ + "instance_permitted": true, + "org_ids": [], + "project_grants": [], + "project_ids": [] + }`, + }, + { + name: "project.read, org ID and project ID permitted", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project_grant.read", "project_grant.write"}, + }, + }, + permm: "project.read", + }, + want: `{ + "instance_permitted": false, + "org_ids": ["orgID"], + "project_ids": ["projectID"], + "project_grants": [] + }`, + }, + { + name: "project_grant.read, project grant ID permitted", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project_grant.read", "project_grant.write"}, + }, + }, + permm: "project_grant.read", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [ + { + "project_id": "projectID", + "grant_id": "grantID" + } + ] + }`, + }, + { + name: "instance without aggregate ID", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "", + Permissions: []string{"foo.bar", "bar.foo"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "wrong instance ID", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "wrong", + Permissions: []string{"foo.bar", "bar.foo"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "permission on other instance", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"bar.foo"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "wrong", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "org ID missing", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "multiple org IDs", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "Org1", + Permissions: []string{"foo.bar"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "Org2", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": ["Org1", "Org2"], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "project ID missing", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeProject, + AggregateID: "", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "multiple project IDs", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeProject, + AggregateID: "P1", + Permissions: []string{"foo.bar"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "P2", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": ["P1", "P2"], + "project_grants": [] + }`, + }, + { + name: "project grant ID missing", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "", + ObjectID: "", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "multiple project IDs", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "P1", + ObjectID: "O1", + Permissions: []string{"foo.bar"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "P2", + ObjectID: "O2", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [ + { + "project_id": "P1", + "grant_id": "O1" + }, + { + "project_id": "P2", + "grant_id": "O2" + } + ] + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rows, err := dbPool.Query(CTX, query, database.NewJSONArray(tt.args.permissions), tt.args.reqInstanceID, tt.args.permm) + require.NoError(t, err) + got, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + require.NoError(t, err) + assert.JSONEq(t, tt.want, got) + }) + } +} + +const ( + instanceID = "instanceID" + orgID = "orgID" + projectID = "projectID" +) + +func TestPermittedOrgs(t *testing.T) { + t.Parallel() + + tx, err := dbPool.Begin(CTX) + require.NoError(t, err) + defer tx.Rollback(CTX) + + // Insert a couple of deterministic field rows to test the function. + // Data will not persist, because the transaction is rolled back. + createRolePermission(t, tx, "IAM_OWNER", []string{"org.write", "org.read"}) + createRolePermission(t, tx, "ORG_OWNER", []string{"org.write", "org.read"}) + createMember(t, tx, instance.AggregateType, "instance_user") + createMember(t, tx, org.AggregateType, "org_user") + + const query = "SELECT instance_permitted, org_ids FROM eventstore.permitted_orgs($1,$2,$3,$4,$5);" + type args struct { + reqInstanceID string + authUserID string + systemUserPerms []authz.SystemUserPermissions + perm string + filterOrg *string + } + type result struct { + InstancePermitted bool + OrgIDs pgtype.FlatArray[string] + } + tests := []struct { + name string + args args + want result + }{ + { + name: "system user, instance", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeSystem, + Permissions: []string{"org.write", "org.read"}, + }}, + perm: "org.read", + }, + want: result{ + InstancePermitted: true, + }, + }, + { + name: "system user, orgs", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeOrganization, + AggregateID: orgID, + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }}, + perm: "org.read", + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "instance member", + args: args{ + reqInstanceID: instanceID, + authUserID: "instance_user", + perm: "org.read", + }, + want: result{ + InstancePermitted: true, + }, + }, + { + name: "org member", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "org.read", + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "org member, filter", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "org.read", + filterOrg: gu.Ptr(orgID), + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "org member, filter wrong org", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "org.read", + filterOrg: gu.Ptr("foobar"), + }, + want: result{}, + }, + { + name: "no permission", + args: args{ + reqInstanceID: instanceID, + authUserID: "foobar", + perm: "org.read", + filterOrg: gu.Ptr(orgID), + }, + want: result{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rows, err := tx.Query(CTX, query, tt.args.reqInstanceID, tt.args.authUserID, database.NewJSONArray(tt.args.systemUserPerms), tt.args.perm, tt.args.filterOrg) + require.NoError(t, err) + got, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByPos[result]) + require.NoError(t, err) + assert.Equal(t, tt.want.InstancePermitted, got.InstancePermitted) + assert.ElementsMatch(t, tt.want.OrgIDs, got.OrgIDs) + }) + } +} + +func TestPermittedProjects(t *testing.T) { + t.Parallel() + + tx, err := dbPool.Begin(CTX) + require.NoError(t, err) + defer tx.Rollback(CTX) + + // Insert a couple of deterministic field rows to test the function. + // Data will not persist, because the transaction is rolled back. + createRolePermission(t, tx, "IAM_OWNER", []string{"project.write", "project.read"}) + createRolePermission(t, tx, "ORG_OWNER", []string{"project.write", "project.read"}) + createRolePermission(t, tx, "PROJECT_OWNER", []string{"project.write", "project.read"}) + createMember(t, tx, instance.AggregateType, "instance_user") + createMember(t, tx, org.AggregateType, "org_user") + createMember(t, tx, project.AggregateType, "project_user") + + const query = "SELECT instance_permitted, org_ids, project_ids FROM eventstore.permitted_projects($1,$2,$3,$4,$5);" + type args struct { + reqInstanceID string + authUserID string + systemUserPerms []authz.SystemUserPermissions + perm string + filterOrg *string + } + type result struct { + InstancePermitted bool + OrgIDs pgtype.FlatArray[string] + ProjectIDs pgtype.FlatArray[string] + } + tests := []struct { + name string + args args + want result + }{ + { + name: "system user, instance", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeSystem, + Permissions: []string{"project.write", "project.read"}, + }}, + perm: "project.read", + }, + want: result{ + InstancePermitted: true, + }, + }, + { + name: "system user, orgs", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeOrganization, + AggregateID: orgID, + Permissions: []string{"project.read", "project.write"}, + }}, + perm: "project.read", + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "system user, projects", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeProject, + AggregateID: projectID, + Permissions: []string{"project.read", "project.write"}, + }}, + perm: "project.read", + }, + want: result{ + ProjectIDs: pgtype.FlatArray[string]{projectID}, + }, + }, + { + name: "system user, org and project", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeOrganization, + AggregateID: orgID, + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: projectID, + Permissions: []string{"project.read", "project.write"}, + }, + }, + perm: "project.read", + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + ProjectIDs: pgtype.FlatArray[string]{projectID}, + }, + }, + { + name: "instance member", + args: args{ + reqInstanceID: instanceID, + authUserID: "instance_user", + perm: "project.read", + }, + want: result{ + InstancePermitted: true, + }, + }, + { + name: "org member", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "project.read", + }, + want: result{ + InstancePermitted: false, + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "org member, filter", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "project.read", + filterOrg: gu.Ptr(orgID), + }, + want: result{ + InstancePermitted: false, + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "org member, filter wrong org", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "project.read", + filterOrg: gu.Ptr("foobar"), + }, + want: result{}, + }, + { + name: "project member", + args: args{ + reqInstanceID: instanceID, + authUserID: "project_user", + perm: "project.read", + }, + want: result{ + ProjectIDs: pgtype.FlatArray[string]{projectID}, + }, + }, + { + name: "no permission", + args: args{ + reqInstanceID: instanceID, + authUserID: "foobar", + perm: "project.read", + filterOrg: gu.Ptr(orgID), + }, + want: result{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rows, err := tx.Query(CTX, query, tt.args.reqInstanceID, tt.args.authUserID, database.NewJSONArray(tt.args.systemUserPerms), tt.args.perm, tt.args.filterOrg) + require.NoError(t, err) + got, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByPos[result]) + require.NoError(t, err) + assert.Equal(t, tt.want.InstancePermitted, got.InstancePermitted) + assert.ElementsMatch(t, tt.want.OrgIDs, got.OrgIDs) + }) + } +} + +func createRolePermission(t *testing.T, tx pgx.Tx, role string, permissions []string) { + for _, perm := range permissions { + createTestField(t, tx, instanceID, permission.AggregateType, instanceID, "role_permission", role, "permission", perm) + } +} + +func createMember(t *testing.T, tx pgx.Tx, aggregateType eventstore.AggregateType, userID string) { + var err error + switch aggregateType { + case instance.AggregateType: + createTestField(t, tx, instanceID, aggregateType, instanceID, "instance_member_role", userID, "instance_role", "IAM_OWNER") + case org.AggregateType: + createTestField(t, tx, orgID, aggregateType, orgID, "org_member_role", userID, "org_role", "ORG_OWNER") + case project.AggregateType: + createTestField(t, tx, orgID, aggregateType, orgID, "project_member_role", userID, "project_role", "PROJECT_OWNER") + default: + panic("unknown aggregate type " + aggregateType) + } + require.NoError(t, err) +} + +func createTestField(t *testing.T, tx pgx.Tx, resourceOwner string, aggregateType eventstore.AggregateType, aggregateID, objectType, objectID, fieldName string, value any) { + const query = `INSERT INTO eventstore.fields( + instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, field_name, value, value_must_be_unique, should_index, object_revision) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, false, true, 1);` + encValue, err := json.Marshal(value) + require.NoError(t, err) + _, err = tx.Exec(CTX, query, instanceID, resourceOwner, aggregateType, aggregateID, objectType, objectID, fieldName, encValue) + require.NoError(t, err) + +} diff --git a/cmd/setup/integration_test/setup_test.go b/cmd/setup/integration_test/setup_test.go new file mode 100644 index 0000000000..42b8502841 --- /dev/null +++ b/cmd/setup/integration_test/setup_test.go @@ -0,0 +1,41 @@ +// Package setup_test implements tests for procedural PostgreSQL functions, +// created in the database during Zitadel setup. +// Tests depend on `zitadel setup` being run first and therefore is run as integration tests. +// A PGX connection is used directly to the integration test database. +// This package assumes the database server available as per integration test defaults. +// See the [ConnString] constant. + +//go:build integration + +package setup_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +const ConnString = "host=localhost port=5432 user=zitadel dbname=zitadel sslmode=disable" + +var ( + CTX context.Context + dbPool *pgxpool.Pool +) + +func TestMain(m *testing.M) { + var cancel context.CancelFunc + CTX, cancel = context.WithTimeout(context.Background(), time.Second*10) + + var err error + dbPool, err = pgxpool.New(context.Background(), ConnString) + if err != nil { + panic(err) + } + exit := m.Run() + cancel() + dbPool.Close() + os.Exit(exit) +} diff --git a/cmd/setup/48_river_queue_repeatable.go b/cmd/setup/river_queue_repeatable.go similarity index 82% rename from cmd/setup/48_river_queue_repeatable.go rename to cmd/setup/river_queue_repeatable.go index e88293256b..bfbd3ee581 100644 --- a/cmd/setup/48_river_queue_repeatable.go +++ b/cmd/setup/river_queue_repeatable.go @@ -13,10 +13,7 @@ type RiverMigrateRepeatable struct { } func (mig *RiverMigrateRepeatable) Execute(ctx context.Context, _ eventstore.Event) error { - if mig.client.Type() != "postgres" { - return nil - } - return queue.New(mig.client).ExecuteMigrations(ctx) + return queue.NewMigrator(mig.client).Execute(ctx) } func (mig *RiverMigrateRepeatable) String() string { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index b78d1fc9cf..f4df9fc71b 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -5,8 +5,13 @@ import ( "embed" _ "embed" "errors" + "fmt" "net/http" + "os" + "os/signal" "path" + "syscall" + "time" "github.com/jackc/pgx/v5/pgconn" "github.com/spf13/cobra" @@ -37,6 +42,7 @@ import ( notify_handler "github.com/zitadel/zitadel/internal/notification" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/queue" es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore" es_v4_pg "github.com/zitadel/zitadel/internal/v2/eventstore/postgres" "github.com/zitadel/zitadel/internal/webauthn" @@ -54,7 +60,7 @@ func New() *cobra.Command { Short: "setup ZITADEL instance", Long: `sets up data to start ZITADEL. Requirements: -- cockroachdb`, +- postgreSQL`, Run: func(cmd *cobra.Command, args []string) { err := tls.ModeFromFlag(cmd) logging.OnError(err).Fatal("invalid tlsMode") @@ -101,12 +107,39 @@ func bindForMirror(cmd *cobra.Command) error { func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) { logging.Info("setup started") - i18n.MustLoadSupportedLanguagesFromDir() + var setupErr error + ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) + defer func() { + stop() + + if setupErr == nil { + logging.Info("setup completed") + return + } + + if setupErr != nil && !errors.Is(setupErr, context.Canceled) { + // If Setup failed for some other reason than the context being cancelled, + // then this could be a fatal error we should not retry + logging.WithFields("error", setupErr).Fatal("setup failed, skipping cleanup") + return + } + + // if we're in the middle of long-running setup, run cleanup before exiting + // so if/when we're restarted we can pick up where we left off rather than + // booting into a broken state that requires manual intervention + // kubernetes will typically kill the pod after 30 seconds if the container does not exit + cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) + defer cleanupCancel() + + Cleanup(cleanupCtx, config) + }() + + i18n.MustLoadSupportedLanguagesFromDir() dbClient, err := database.Connect(config.Database, false) logging.OnError(err).Fatal("unable to connect to database") - config.Eventstore.Querier = old_es.NewCRDB(dbClient) + config.Eventstore.Querier = old_es.NewPostgres(dbClient) esV3 := new_es.NewEventstore(dbClient) config.Eventstore.Pusher = esV3 config.Eventstore.Searcher = esV3 @@ -136,7 +169,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s5LastFailed = &LastFailed{dbClient: dbClient.DB} steps.s6OwnerRemoveColumns = &OwnerRemoveColumns{dbClient: dbClient.DB} - steps.s7LogstoreTables = &LogstoreTables{dbClient: dbClient.DB, username: config.Database.Username(), dbType: config.Database.Type()} + steps.s7LogstoreTables = &LogstoreTables{dbClient: dbClient.DB, username: config.Database.Username()} steps.s8AuthTokens = &AuthTokenIndexes{dbClient: dbClient} steps.CorrectCreationDate.dbClient = dbClient steps.s12AddOTPColumns = &AddOTPColumns{dbClient: dbClient} @@ -173,37 +206,16 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s45CorrectProjectOwners = &CorrectProjectOwners{eventstore: eventstoreClient} steps.s46InitPermissionFunctions = &InitPermissionFunctions{eventstoreClient: dbClient} steps.s47FillMembershipFields = &FillMembershipFields{eventstore: eventstoreClient} + steps.s48Apps7SAMLConfigsLoginVersion = &Apps7SAMLConfigsLoginVersion{dbClient: dbClient} + steps.s49InitPermittedOrgsFunction = &InitPermittedOrgsFunction{eventstoreClient: dbClient} + steps.s50IDPTemplate6UsePKCE = &IDPTemplate6UsePKCE{dbClient: dbClient} + steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient} + steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient} + steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") - repeatableSteps := []migration.RepeatableMigration{ - &externalConfigChange{ - es: eventstoreClient, - ExternalDomain: config.ExternalDomain, - ExternalPort: config.ExternalPort, - ExternalSecure: config.ExternalSecure, - defaults: config.SystemDefaults, - }, - &projectionTables{ - es: eventstoreClient, - Version: build.Version(), - }, - &DeleteStaleOrgFields{ - eventstore: eventstoreClient, - }, - &FillFieldsForInstanceDomains{ - eventstore: eventstoreClient, - }, - &SyncRolePermissions{ - eventstore: eventstoreClient, - rolePermissionMappings: config.InternalAuthZ.RolePermissionMappings, - }, - &RiverMigrateRepeatable{ - client: dbClient, - }, - } - for _, step := range []migration.Migration{ steps.s14NewEventsTable, steps.s40InitPushFunc, @@ -211,6 +223,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s2AssetsTable, steps.s28AddFieldTable, steps.s31AddAggregateIndexToFields, + steps.s46InitPermissionFunctions, steps.FirstInstance, steps.s5LastFailed, steps.s6OwnerRemoveColumns, @@ -235,14 +248,54 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s38BackChannelLogoutNotificationStart, steps.s44ReplaceCurrentSequencesIndex, steps.s45CorrectProjectOwners, - steps.s46InitPermissionFunctions, steps.s47FillMembershipFields, + steps.s49InitPermittedOrgsFunction, + steps.s50IDPTemplate6UsePKCE, + steps.s51IDPTemplate6RootCA, + steps.s52IDPTemplate6LDAP2, + steps.s53InitPermittedOrgsFunction, } { - mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") + setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") + if setupErr != nil { + return + } + } + + commands, _, _, _ := startCommandsQueries(ctx, eventstoreClient, eventstoreV4, dbClient, masterKey, config) + + repeatableSteps := []migration.RepeatableMigration{ + &externalConfigChange{ + es: eventstoreClient, + ExternalDomain: config.ExternalDomain, + ExternalPort: config.ExternalPort, + ExternalSecure: config.ExternalSecure, + defaults: config.SystemDefaults, + }, + &projectionTables{ + es: eventstoreClient, + Version: build.Version(), + }, + &DeleteStaleOrgFields{ + eventstore: eventstoreClient, + }, + &FillFieldsForInstanceDomains{ + eventstore: eventstoreClient, + }, + &SyncRolePermissions{ + commands: commands, + eventstore: eventstoreClient, + rolePermissionMappings: config.InternalAuthZ.RolePermissionMappings, + }, + &RiverMigrateRepeatable{ + client: dbClient, + }, } for _, repeatableStep := range repeatableSteps { - mustExecuteMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step") + setupErr = executeMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step") + if setupErr != nil { + return + } } // These steps are executed after the repeatable steps because they add fields projections @@ -256,28 +309,27 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s37Apps7OIDConfigsBackChannelLogoutURI, steps.s42Apps7OIDCConfigsLoginVersion, steps.s43CreateFieldsDomainIndex, + steps.s48Apps7SAMLConfigsLoginVersion, } { - mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") + setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") + if setupErr != nil { + return + } } // projection initialization must be done last, since the steps above might add required columns to the projections if !config.ForMirror && config.InitProjections.Enabled { - initProjections( - ctx, - eventstoreClient, - eventstoreV4, - dbClient, - dbClient, - masterKey, - config, - ) + setupErr = initProjections(ctx, eventstoreClient) + if setupErr != nil { + return + } } } -func mustExecuteMigration(ctx context.Context, eventstoreClient *eventstore.Eventstore, step migration.Migration, errorMsg string) { +func executeMigration(ctx context.Context, eventstoreClient *eventstore.Eventstore, step migration.Migration, errorMsg string) error { err := migration.Migrate(ctx, eventstoreClient, step) if err == nil { - return + return nil } logFields := []any{ "name", step.String(), @@ -292,15 +344,16 @@ func mustExecuteMigration(ctx context.Context, eventstoreClient *eventstore.Even "hint", pgErr.Hint, ) } - logging.WithFields(logFields...).WithError(err).Fatal(errorMsg) + logging.WithFields(logFields...).WithError(err).Error(errorMsg) + return fmt.Errorf("%s: %w", errorMsg, err) } // readStmt reads a single file from the embedded FS, // under the folder/typ/filename path. // Typ describes the database dialect and may be omitted if no // dialect specific migration is specified. -func readStmt(fs embed.FS, folder, typ, filename string) (string, error) { - stmt, err := fs.ReadFile(path.Join(folder, typ, filename)) +func readStmt(fs embed.FS, folder, filename string) (string, error) { + stmt, err := fs.ReadFile(path.Join(folder, filename)) return string(stmt), err } @@ -313,16 +366,15 @@ type statement struct { // under the folder/type path. // Typ describes the database dialect and may be omitted if no // dialect specific migration is specified. -func readStatements(fs embed.FS, folder, typ string) ([]statement, error) { - basePath := path.Join(folder, typ) - dir, err := fs.ReadDir(basePath) +func readStatements(fs embed.FS, folder string) ([]statement, error) { + dir, err := fs.ReadDir(folder) if err != nil { return nil, err } statements := make([]statement, len(dir)) for i, file := range dir { statements[i].file = file.Name() - statements[i].query, err = readStmt(fs, folder, typ, file.Name()) + statements[i].query, err = readStmt(fs, folder, file.Name()) if err != nil { return nil, err } @@ -330,18 +382,20 @@ func readStatements(fs embed.FS, folder, typ string) ([]statement, error) { return statements, nil } -func initProjections( +func startCommandsQueries( ctx context.Context, eventstoreClient *eventstore.Eventstore, eventstoreV4 *es_v4.EventStore, - queryDBClient, - projectionDBClient *database.DB, + dbClient *database.DB, masterKey string, config *Config, +) ( + *command.Commands, + *query.Queries, + *admin_view.View, + *auth_view.View, ) { - logging.Info("init-projections is currently in beta") - - keyStorage, err := cryptoDB.NewKeyStorage(queryDBClient, masterKey) + keyStorage, err := cryptoDB.NewKeyStorage(dbClient, masterKey) logging.OnError(err).Fatal("unable to start key storage") keys, err := encryption.EnsureEncryptionKeys(ctx, config.EncryptionKeys, keyStorage) @@ -349,7 +403,7 @@ func initProjections( err = projection.Create( ctx, - queryDBClient, + dbClient, eventstoreClient, projection.Config{ RetryFailedAfter: config.InitProjections.RetryFailedAfter, @@ -361,19 +415,15 @@ func initProjections( config.SystemAPIUsers, ) logging.OnError(err).Fatal("unable to start projections") - for _, p := range projection.Projections() { - err := migration.Migrate(ctx, eventstoreClient, p) - logging.WithFields("name", p.String()).OnError(err).Fatal("migration failed") - } - staticStorage, err := config.AssetStorage.NewStorage(queryDBClient.DB) + staticStorage, err := config.AssetStorage.NewStorage(dbClient.DB) logging.OnError(err).Fatal("unable to start asset storage") - adminView, err := admin_view.StartView(queryDBClient) + adminView, err := admin_view.StartView(dbClient) logging.OnError(err).Fatal("unable to start admin view") admin_handler.Register(ctx, admin_handler.Config{ - Client: queryDBClient, + Client: dbClient, Eventstore: eventstoreClient, BulkLimit: config.InitProjections.BulkLimit, FailureCountUntilSkip: uint64(config.InitProjections.MaxFailureCount), @@ -381,22 +431,18 @@ func initProjections( adminView, staticStorage, ) - for _, p := range admin_handler.Projections() { - err := migration.Migrate(ctx, eventstoreClient, p) - logging.WithFields("name", p.String()).OnError(err).Fatal("migration failed") - } sessionTokenVerifier := internal_authz.SessionTokenVerifier(keys.OIDC) - cacheConnectors, err := connector.StartConnectors(config.Caches, queryDBClient) + cacheConnectors, err := connector.StartConnectors(config.Caches, dbClient) logging.OnError(err).Fatal("unable to start caches") queries, err := query.StartQueries( ctx, eventstoreClient, eventstoreV4.Querier, - queryDBClient, - projectionDBClient, + dbClient, + dbClient, cacheConnectors, config.Projections, config.SystemDefaults, @@ -409,7 +455,7 @@ func initProjections( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, // not needed for projections @@ -418,11 +464,11 @@ func initProjections( ) logging.OnError(err).Fatal("unable to start queries") - authView, err := auth_view.StartView(queryDBClient, keys.OIDC, queries, eventstoreClient) + authView, err := auth_view.StartView(dbClient, keys.OIDC, queries, eventstoreClient) logging.OnError(err).Fatal("unable to start admin view") auth_handler.Register(ctx, auth_handler.Config{ - Client: queryDBClient, + Client: dbClient, Eventstore: eventstoreClient, BulkLimit: config.InitProjections.BulkLimit, FailureCountUntilSkip: uint64(config.InitProjections.MaxFailureCount), @@ -430,16 +476,13 @@ func initProjections( authView, queries, ) - for _, p := range auth_handler.Projections() { - err := migration.Migrate(ctx, eventstoreClient, p) - logging.WithFields("name", p.String()).OnError(err).Fatal("migration failed") - } - authZRepo, err := authz.Start(queries, eventstoreClient, queryDBClient, keys.OIDC, config.ExternalSecure) + authZRepo, err := authz.Start(queries, eventstoreClient, dbClient, keys.OIDC, config.ExternalSecure) logging.OnError(err).Fatal("unable to start authz repo") permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } + commands, err := command.StartCommands(ctx, eventstoreClient, cacheConnectors, @@ -471,6 +514,12 @@ func initProjections( config.DefaultInstance.SecretGenerators, ) logging.OnError(err).Fatal("unable to start commands") + + q, err := queue.NewQueue(&queue.Config{ + Client: dbClient, + }) + logging.OnError(err).Fatal("unable to init queue") + notify_handler.Register( ctx, config.Projections.Customizations["notifications"], @@ -492,10 +541,45 @@ func initProjections( keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, - queryDBClient, + q, ) - for _, p := range notify_handler.Projections() { - err := migration.Migrate(ctx, eventstoreClient, p) - logging.WithFields("name", p.String()).OnError(err).Fatal("migration failed") - } + + return commands, queries, adminView, authView +} + +func initProjections( + ctx context.Context, + eventstoreClient *eventstore.Eventstore, +) error { + logging.Info("init-projections is currently in beta") + + for _, p := range projection.Projections() { + if err := migration.Migrate(ctx, eventstoreClient, p); err != nil { + logging.WithFields("name", p.String()).OnError(err).Error("projection migration failed") + return err + } + } + + for _, p := range admin_handler.Projections() { + if err := migration.Migrate(ctx, eventstoreClient, p); err != nil { + logging.WithFields("name", p.String()).OnError(err).Error("admin schema migration failed") + return err + } + } + + for _, p := range auth_handler.Projections() { + if err := migration.Migrate(ctx, eventstoreClient, p); err != nil { + logging.WithFields("name", p.String()).OnError(err).Error("auth schema migration failed") + return err + } + } + + for _, p := range notify_handler.Projections() { + if err := migration.Migrate(ctx, eventstoreClient, p); err != nil { + logging.WithFields("name", p.String()).OnError(err).Error("notification migration failed") + return err + } + } + + return nil } diff --git a/cmd/setup/sync_role_permissions.go b/cmd/setup/sync_role_permissions.go index b38b075d82..5c380265b5 100644 --- a/cmd/setup/sync_role_permissions.go +++ b/cmd/setup/sync_role_permissions.go @@ -2,29 +2,22 @@ package setup import ( "context" - "database/sql" _ "embed" "fmt" - "strings" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/repository/permission" -) - -var ( - //go:embed sync_role_permissions.sql - getRolePermissionOperationsQuery string ) // SyncRolePermissions is a repeatable step which synchronizes the InternalAuthZ // RolePermissionMappings from the configuration to the database. // This is needed until role permissions are manageable over the API. type SyncRolePermissions struct { + commands *command.Commands eventstore *eventstore.Eventstore rolePermissionMappings []authz.RoleMapping } @@ -38,18 +31,11 @@ func (mig *SyncRolePermissions) Execute(ctx context.Context, _ eventstore.Event) func (mig *SyncRolePermissions) executeSystem(ctx context.Context) error { logging.WithFields("migration", mig.String()).Info("prepare system role permission sync events") - - target := rolePermissionMappingsToDatabaseMap(mig.rolePermissionMappings, true) - cmds, err := mig.synchronizeCommands(ctx, "SYSTEM", target) + details, err := mig.commands.SynchronizeRolePermission(ctx, "SYSTEM", mig.rolePermissionMappings) if err != nil { return err } - events, err := mig.eventstore.Push(ctx, cmds...) - if err != nil { - return err - } - - logging.WithFields("migration", mig.String(), "pushed_events", len(events)).Info("pushed system role permission sync events") + logging.WithFields("migration", mig.String(), "sequence", details.Sequence).Info("pushed system role permission sync events") return nil } @@ -70,51 +56,17 @@ func (mig *SyncRolePermissions) executeInstances(ctx context.Context) error { if err != nil { return err } - target := rolePermissionMappingsToDatabaseMap(mig.rolePermissionMappings, false) for i, instanceID := range instances { logging.WithFields("instance_id", instanceID, "migration", mig.String(), "progress", fmt.Sprintf("%d/%d", i+1, len(instances))).Info("prepare instance role permission sync events") - cmds, err := mig.synchronizeCommands(ctx, instanceID, target) + details, err := mig.commands.SynchronizeRolePermission(ctx, instanceID, mig.rolePermissionMappings) if err != nil { return err } - events, err := mig.eventstore.Push(ctx, cmds...) - if err != nil { - return err - } - logging.WithFields("instance_id", instanceID, "migration", mig.String(), "pushed_events", len(events)).Info("pushed instance role permission sync events") + logging.WithFields("instance_id", instanceID, "migration", mig.String(), "sequence", details.Sequence).Info("pushed instance role permission sync events") } return nil } -// synchronizeCommands checks the current state of role permissions in the eventstore for the aggregate. -// It returns the commands required to reach the desired state passed in target. -// For system level permissions aggregateID must be set to `SYSTEM`, -// else it is the instance ID. -func (mig *SyncRolePermissions) synchronizeCommands(ctx context.Context, aggregateID string, target database.Map[[]string]) (cmds []eventstore.Command, err error) { - aggregate := permission.NewAggregate(aggregateID) - err = mig.eventstore.Client().QueryContext(ctx, func(rows *sql.Rows) error { - for rows.Next() { - var operation, role, perm string - if err := rows.Scan(&operation, &role, &perm); err != nil { - return err - } - logging.WithFields("aggregate_id", aggregateID, "migration", mig.String(), "operation", operation, "role", role, "permission", perm).Debug("sync role permission") - switch operation { - case "add": - cmds = append(cmds, permission.NewAddedEvent(ctx, aggregate, role, perm)) - case "remove": - cmds = append(cmds, permission.NewRemovedEvent(ctx, aggregate, role, perm)) - } - } - return rows.Close() - - }, getRolePermissionOperationsQuery, aggregateID, target) - if err != nil { - return nil, err - } - return cmds, err -} - func (*SyncRolePermissions) String() string { return "repeatable_sync_role_permissions" } @@ -122,13 +74,3 @@ func (*SyncRolePermissions) String() string { func (*SyncRolePermissions) Check(lastRun map[string]interface{}) bool { return true } - -func rolePermissionMappingsToDatabaseMap(mappings []authz.RoleMapping, system bool) database.Map[[]string] { - out := make(database.Map[[]string], len(mappings)) - for _, m := range mappings { - if system == strings.HasPrefix(m.Role, "SYSTEM") { - out[m.Role] = m.Permissions - } - } - return out -} diff --git a/cmd/start/config.go b/cmd/start/config.go index 910759b653..78b6f0afe0 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/saml" @@ -27,6 +27,7 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/notification/handlers" @@ -56,6 +57,7 @@ type Config struct { Profiler profiler.Config Projections projection.Config Notifications handlers.WorkerConfig + Executions execution.WorkerConfig Auth auth_es.Config Admin admin_es.Config UserAgentCookie *middleware.UserAgentCookieConfig @@ -65,12 +67,13 @@ type Config struct { Login login.Config Console console.Config AssetStorage static_config.AssetStorageConfig - InternalAuthZ internal_authz.Config + InternalAuthZ authz.Config + SystemAuthZ authz.Config SystemDefaults systemdefaults.SystemDefaults EncryptionKeys *encryption.EncryptionKeyConfig DefaultInstance command.InstanceSetup AuditLogRetention time.Duration - SystemAPIUsers map[string]*internal_authz.SystemAPIUser + SystemAPIUsers map[string]*authz.SystemAPIUser CustomerPortal string Machine *id.Config Actions *actions.Config @@ -94,12 +97,12 @@ func MustNewConfig(v *viper.Viper) *Config { err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( hooks.SliceTypeStringDecode[*domain.CustomMessageText], - hooks.SliceTypeStringDecode[internal_authz.RoleMapping], - hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.SliceTypeStringDecode[authz.RoleMapping], + hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], hooks.MapHTTPHeaderStringDecode, - database.DecodeHook, + database.DecodeHook(false), actions.HTTPConfigDecodeHook, - hook.EnumHookFunc(internal_authz.MemberTypeString), + hook.EnumHookFunc(authz.MemberTypeString), hooks.MapTypeStringDecode[domain.Feature, any], hooks.SliceTypeStringDecode[*command.SetQuota], hook.Base64ToBytesHookFunc(), @@ -125,7 +128,9 @@ func MustNewConfig(v *viper.Viper) *Config { logging.OnError(err).Fatal("unable to set profiler") id.Configure(config.Machine) - actions.SetHTTPConfig(&config.Actions.HTTP) + if config.Actions != nil { + actions.SetHTTPConfig(&config.Actions.HTTP) + } // Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API. config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings diff --git a/cmd/start/start.go b/cmd/start/start.go index 4091213d2d..e3d84625b4 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -34,6 +34,7 @@ import ( "github.com/zitadel/zitadel/internal/api" "github.com/zitadel/zitadel/internal/api/assets" internal_authz "github.com/zitadel/zitadel/internal/api/authz" + action_v2_beta "github.com/zitadel/zitadel/internal/api/grpc/action/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/admin" "github.com/zitadel/zitadel/internal/api/grpc/auth" feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" @@ -44,11 +45,9 @@ import ( oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" - action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events" user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha" - "github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3" saml_v2 "github.com/zitadel/zitadel/internal/api/grpc/saml/v2" session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" @@ -57,6 +56,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/system" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" + webkey "github.com/zitadel/zitadel/internal/api/grpc/webkey/v2beta" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/idp" @@ -81,17 +81,19 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/emitters/access" - "github.com/zitadel/zitadel/internal/logstore/emitters/execution" - "github.com/zitadel/zitadel/internal/logstore/emitters/stdout" + emit_execution "github.com/zitadel/zitadel/internal/logstore/emitters/execution" + emit_stdout "github.com/zitadel/zitadel/internal/logstore/emitters/stdout" "github.com/zitadel/zitadel/internal/logstore/record" "github.com/zitadel/zitadel/internal/net" "github.com/zitadel/zitadel/internal/notification" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/queue" "github.com/zitadel/zitadel/internal/static" es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore" es_v4_pg "github.com/zitadel/zitadel/internal/v2/eventstore/postgres" @@ -105,7 +107,7 @@ func New(server chan<- *Server) *cobra.Command { Short: "starts ZITADEL instance", Long: `starts ZITADEL. Requirements: -- cockroachdb`, +- postgreSQL`, RunE: func(cmd *cobra.Command, args []string) error { err := cmd_tls.ModeFromFlag(cmd) if err != nil { @@ -143,10 +145,6 @@ type Server struct { func startZitadel(ctx context.Context, config *Config, masterKey string, server chan<- *Server) error { showBasicInformation(config) - // sink Server is stubbed out in production builds, see function's godoc. - closeSink := sink.StartServer() - defer closeSink() - i18n.MustLoadSupportedLanguagesFromDir() dbClient, err := database.Connect(config.Database, false) @@ -165,7 +163,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server config.Eventstore.Pusher = new_es.NewEventstore(dbClient) config.Eventstore.Searcher = new_es.NewEventstore(dbClient) - config.Eventstore.Querier = old_es.NewCRDB(dbClient) + config.Eventstore.Querier = old_es.NewPostgres(dbClient) eventstoreClient := eventstore.NewEventstore(config.Eventstore) eventstoreV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(dbClient, &es_v4_pg.Config{ MaxRetries: config.Eventstore.MaxRetries, @@ -195,7 +193,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, config.AuditLogRetention, @@ -211,7 +209,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server return fmt.Errorf("error starting authz repo: %w", err) } permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } storage, err := config.AssetStorage.NewStorage(dbClient.DB) @@ -254,12 +252,17 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server } defer commands.Close(ctx) // wait for background jobs + // sink Server is stubbed out in production builds, see function's godoc. + closeSink := sink.StartServer(commands) + defer closeSink() + clock := clockpkg.New() - actionsExecutionStdoutEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.ExecutionLog]()) + actionsExecutionStdoutEmitter, err := logstore.NewEmitter(ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, emit_stdout.NewStdoutEmitter[*record.ExecutionLog]()) if err != nil { return err } - actionsExecutionDBEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, config.Quotas.Execution, execution.NewDatabaseLogStorage(dbClient, commands, queries)) + + actionsExecutionDBEmitter, err := logstore.NewEmitter(ctx, clock, config.Quotas.Execution, emit_execution.NewDatabaseLogStorage(dbClient, commands, queries)) if err != nil { return err } @@ -267,6 +270,13 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server actionsLogstoreSvc := logstore.New(queries, actionsExecutionDBEmitter, actionsExecutionStdoutEmitter) actions.SetLogstoreService(actionsLogstoreSvc) + q, err := queue.NewQueue(&queue.Config{ + Client: dbClient, + }) + if err != nil { + return err + } + notification.Register( ctx, config.Projections.Customizations["notifications"], @@ -288,10 +298,24 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, - dbClient, + q, ) notification.Start(ctx) + execution.Register( + ctx, + config.Projections.Customizations["executions"], + config.Executions, + queries, + eventstoreClient.EventTypes(), + q, + ) + execution.Start(ctx) + + if err = q.Start(ctx); err != nil { + return err + } + router := mux.NewRouter() tlsConfig, err := config.TLS.Config() if err != nil { @@ -378,23 +402,23 @@ func startAPIs( return nil, err } - accessStdoutEmitter, err := logstore.NewEmitter[*record.AccessLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Access.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.AccessLog]()) + accessStdoutEmitter, err := logstore.NewEmitter(ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Access.Stdout.Enabled}, emit_stdout.NewStdoutEmitter[*record.AccessLog]()) if err != nil { return nil, err } - accessDBEmitter, err := logstore.NewEmitter[*record.AccessLog](ctx, clock, &config.Quotas.Access.EmitterConfig, access.NewDatabaseLogStorage(dbClient, commands, queries)) + accessDBEmitter, err := logstore.NewEmitter(ctx, clock, &config.Quotas.Access.EmitterConfig, access.NewDatabaseLogStorage(dbClient, commands, queries)) if err != nil { return nil, err } - accessSvc := logstore.New[*record.AccessLog](queries, accessDBEmitter, accessStdoutEmitter) + accessSvc := logstore.New(queries, accessDBEmitter, accessStdoutEmitter) exhaustedCookieHandler := http_util.NewCookieHandler( http_util.WithUnsecure(), http_util.WithNonHttpOnly(), http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))), ) limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig) - apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) + apis, err := api.New(ctx, config.Port, router, queries, verifier, config.SystemAuthZ, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) if err != nil { return nil, fmt.Errorf("error creating api %w", err) } @@ -460,7 +484,7 @@ func startAPIs( if err := apis.RegisterService(ctx, idp_v2.CreateServer(commands, queries, permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { + if err := apis.RegisterService(ctx, action_v2_beta.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { @@ -477,7 +501,7 @@ func startAPIs( } instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) - apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) + apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler)) @@ -521,7 +545,7 @@ func startAPIs( keys.User, &config.SCIM, instanceInterceptor.HandlerFuncWithError, - middleware.AuthorizationInterceptor(verifier, config.InternalAuthZ).HandlerFuncWithError)) + middleware.AuthorizationInterceptor(verifier, config.SystemAuthZ, config.InternalAuthZ).HandlerFuncWithError)) c, err := console.Start(config.Console, config.ExternalSecure, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal) if err != nil { @@ -560,7 +584,7 @@ func startAPIs( if err := apis.RegisterService(ctx, oidc_v2beta.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil { + if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure, keys.OIDC)); err != nil { return nil, err } // After SAML provider so that the callback endpoint can be used @@ -587,7 +611,7 @@ func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls go func() { logging.Infof("server is listening on %s", lis.Addr().String()) if tlsConfig != nil { - //we don't need to pass the files here, because we already initialized the TLS config on the server + // we don't need to pass the files here, because we already initialized the TLS config on the server errCh <- http1Server.ServeTLS(lis, "", "") } else { errCh <- http1Server.Serve(lis) diff --git a/cmd/start/start_from_init.go b/cmd/start/start_from_init.go index 38a6a6c4d1..62d705b33c 100644 --- a/cmd/start/start_from_init.go +++ b/cmd/start/start_from_init.go @@ -21,7 +21,7 @@ Second the initial events are created. Last ZITADEL starts. Requirements: -- cockroachdb`, +- postgreSQL`, Run: func(cmd *cobra.Command, args []string) { err := tls.ModeFromFlag(cmd) logging.OnError(err).Fatal("invalid tlsMode") diff --git a/console/angular.json b/console/angular.json index 278498ccd7..5564b2c428 100644 --- a/console/angular.json +++ b/console/angular.json @@ -63,7 +63,7 @@ { "type": "initial", "maximumWarning": "8mb", - "maximumError": "9mb" + "maximumError": "10mb" }, { "type": "anyComponentStyle", diff --git a/console/package.json b/console/package.json index fcf3a4bbf8..2d986730c2 100644 --- a/console/package.json +++ b/console/package.json @@ -12,43 +12,43 @@ }, "private": true, "dependencies": { - "@angular/animations": "^16.2.5", - "@angular/cdk": "^16.2.4", - "@angular/common": "^16.2.5", - "@angular/compiler": "^16.2.5", - "@angular/core": "^16.2.5", - "@angular/forms": "^16.2.5", - "@angular/material": "^16.2.4", - "@angular/material-moment-adapter": "^16.2.4", - "@angular/platform-browser": "^16.2.5", - "@angular/platform-browser-dynamic": "^16.2.5", - "@angular/router": "^16.2.5", - "@angular/service-worker": "^16.2.5", + "@angular/animations": "^16.2.12", + "@angular/cdk": "^16.2.14", + "@angular/common": "^16.2.12", + "@angular/compiler": "^16.2.12", + "@angular/core": "^16.2.12", + "@angular/forms": "^16.2.12", + "@angular/material": "^16.2.14", + "@angular/material-moment-adapter": "^16.2.14", + "@angular/platform-browser": "^16.2.12", + "@angular/platform-browser-dynamic": "^16.2.12", + "@angular/router": "^16.2.12", + "@angular/service-worker": "^16.2.12", + "@connectrpc/connect": "^2.0.0", + "@connectrpc/connect-web": "^2.0.0", "@ctrl/ngx-codemirror": "^6.1.0", "@fortawesome/angular-fontawesome": "^0.13.0", - "@fortawesome/fontawesome-svg-core": "^6.4.2", - "@fortawesome/free-brands-svg-icons": "^6.4.2", - "@grpc/grpc-js": "^1.11.2", - "@netlify/framework-info": "^9.8.13", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", "@ngx-translate/core": "^15.0.0", + "@zitadel/client": "^1.0.7", + "@zitadel/proto": "1.0.5-sha-4118a9d", "angular-oauth2-oidc": "^15.0.1", - "angularx-qrcode": "^16.0.0", + "angularx-qrcode": "^16.0.2", "buffer": "^6.0.3", - "codemirror": "^5.65.8", - "cors": "^2.8.5", + "codemirror": "^5.65.19", "file-saver": "^2.0.5", - "flag-icons": "^7.1.0", - "google-proto-files": "^4.0.0", - "google-protobuf": "^3.21.2", - "grpc-web": "^1.4.1", - "i18n-iso-countries": "^7.7.0", - "libphonenumber-js": "^1.11.8", - "material-design-icons-iconfont": "^6.1.1", - "moment": "^2.29.4", + "flag-icons": "^7.3.2", + "google-protobuf": "^3.21.4", + "grpc-web": "^1.5.0", + "i18n-iso-countries": "^7.14.0", + "libphonenumber-js": "^1.12.6", + "material-design-icons-iconfont": "^6.7.0", + "moment": "^2.30.1", "ngx-color": "^9.0.0", "opentype.js": "^1.3.4", - "posthog-js": "^1.191.0", - "rxjs": "~7.8.0", + "posthog-js": "^1.232.7", + "rxjs": "^7.8.2", "tinycolor2": "^1.6.0", "tslib": "^2.7.0", "uuid": "^10.0.0", @@ -65,6 +65,7 @@ "@angular/compiler-cli": "^16.2.5", "@angular/language-service": "^18.2.4", "@bufbuild/buf": "^1.41.0", + "@netlify/framework-info": "^9.8.13", "@types/file-saver": "^2.0.7", "@types/google-protobuf": "^3.15.3", "@types/jasmine": "~5.1.4", @@ -75,19 +76,17 @@ "@types/qrcode": "^1.5.2", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.60.1", - "codelyzer": "^6.0.2", - "eslint": "^8.50.0", - "jasmine-core": "~5.3.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.57.1", + "jasmine-core": "~5.6.0", "jasmine-spec-reporter": "~7.0.0", - "karma": "^6.4.2", + "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "^5.1.0", "karma-jasmine-html-reporter": "^2.1.0", - "prettier": "^3.1.1", - "prettier-plugin-organize-imports": "^4.0.0", - "protractor": "~7.0.0", + "prettier": "^3.5.3", + "prettier-plugin-organize-imports": "^4.1.0", "typescript": "5.1" } } diff --git a/console/src/app/app-routing.module.ts b/console/src/app/app-routing.module.ts index 4900c8e424..582f65d8af 100644 --- a/console/src/app/app-routing.module.ts +++ b/console/src/app/app-routing.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from './guards/auth.guard'; -import { RoleGuard } from './guards/role.guard'; +import { authGuard } from './guards/auth.guard'; +import { roleGuard } from './guards/role-guard'; import { UserGrantContext } from './modules/user-grants/user-grants-datasource'; import { OrgCreateComponent } from './pages/org-create/org-create.component'; @@ -10,7 +10,7 @@ const routes: Routes = [ { path: '', loadChildren: () => import('./pages/home/home.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['.'], }, @@ -22,7 +22,7 @@ const routes: Routes = [ { path: 'orgs/create', component: OrgCreateComponent, - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['(org.create)?(iam.write)?'], }, @@ -31,12 +31,12 @@ const routes: Routes = [ { path: 'orgs', loadChildren: () => import('./pages/org-list/org-list.module'), - canActivate: [AuthGuard], + canActivate: [authGuard], }, { path: 'granted-projects', loadChildren: () => import('./pages/projects/granted-projects/granted-projects.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['project.grant.read'], }, @@ -44,20 +44,20 @@ const routes: Routes = [ { path: 'projects', loadChildren: () => import('./pages/projects/projects.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['project.read'], }, }, { path: 'users', - canActivate: [AuthGuard], + canActivate: [authGuard], loadChildren: () => import('src/app/pages/users/users.module'), }, { path: 'instance', loadChildren: () => import('./pages/instance/instance.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['iam.read', 'iam.write'], }, @@ -65,7 +65,7 @@ const routes: Routes = [ { path: 'org', loadChildren: () => import('./pages/orgs/org.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['org.read'], }, @@ -73,7 +73,7 @@ const routes: Routes = [ { path: 'actions', loadChildren: () => import('./pages/actions/actions.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['org.action.read', 'org.flow.read'], }, @@ -81,7 +81,7 @@ const routes: Routes = [ { path: 'grants', loadChildren: () => import('./pages/grants/grants.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { context: UserGrantContext.NONE, roles: ['user.grant.read'], @@ -89,12 +89,12 @@ const routes: Routes = [ }, { path: 'grant-create', - canActivate: [AuthGuard], + canActivate: [authGuard], children: [ { path: 'project/:projectid/grant/:grantid', loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'), - canActivate: [RoleGuard], + canActivate: [roleGuard], data: { roles: ['user.grant.write'], }, @@ -102,7 +102,7 @@ const routes: Routes = [ { path: 'project/:projectid', loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'), - canActivate: [RoleGuard], + canActivate: [roleGuard], data: { roles: ['user.grant.write'], }, @@ -110,7 +110,7 @@ const routes: Routes = [ { path: 'user/:userid', loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'), - canActivate: [RoleGuard], + canActivate: [roleGuard], data: { roles: ['user.grant.write'], }, @@ -118,7 +118,7 @@ const routes: Routes = [ { path: '', loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'), - canActivate: [RoleGuard], + canActivate: [roleGuard], data: { roles: ['user.grant.write'], }, @@ -128,7 +128,7 @@ const routes: Routes = [ { path: 'org-settings', loadChildren: () => import('./pages/org-settings/org-settings.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['policy.read'], }, diff --git a/console/src/app/app.component.html b/console/src/app/app.component.html index 5b31b33dc4..9907e233e1 100644 --- a/console/src/app/app.component.html +++ b/console/src/app/app.component.html @@ -1,9 +1,8 @@
- + @@ -12,9 +11,8 @@ id="mainnav" class="nav" [ngClass]="{ shadow: yoffset > 60 }" - *ngIf="user && user !== {}" [org]="org" - [user]="$any(user)" + [user]="user" [isDarkTheme]="componentCssClass === 'dark-theme'" > diff --git a/console/src/app/app.component.ts b/console/src/app/app.component.ts index 24dedf2b5d..bd46e30cee 100644 --- a/console/src/app/app.component.ts +++ b/console/src/app/app.component.ts @@ -1,14 +1,14 @@ import { BreakpointObserver } from '@angular/cdk/layout'; import { OverlayContainer } from '@angular/cdk/overlay'; import { DOCUMENT, ViewportScroller } from '@angular/common'; -import { Component, HostBinding, HostListener, Inject, OnDestroy, ViewChild } from '@angular/core'; +import { Component, DestroyRef, HostBinding, HostListener, Inject, OnDestroy, ViewChild } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { MatDrawer } from '@angular/material/sidenav'; import { DomSanitizer } from '@angular/platform-browser'; import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; import { LangChangeEvent, TranslateService } from '@ngx-translate/core'; -import { Observable, of, Subject } from 'rxjs'; -import { filter, map, takeUntil } from 'rxjs/operators'; +import { Observable, of, Subject, switchMap } from 'rxjs'; +import { filter, map, startWith, takeUntil, tap } from 'rxjs/operators'; import { accountCard, adminLineAnimation, navAnimations, routeAnimations, toolbarAnimation } from './animations'; import { Org } from './proto/generated/zitadel/org_pb'; @@ -21,6 +21,7 @@ import { ThemeService } from './services/theme.service'; import { UpdateService } from './services/update.service'; import { fallbackLanguage, supportedLanguages, supportedLanguagesRegexp } from './utils/language'; import { PosthogService } from './services/posthog.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'cnsl-root', @@ -28,7 +29,7 @@ import { PosthogService } from './services/posthog.service'; styleUrls: ['./app.component.scss'], animations: [toolbarAnimation, ...navAnimations, accountCard, routeAnimations, adminLineAnimation], }) -export class AppComponent implements OnDestroy { +export class AppComponent { @ViewChild('drawer') public drawer!: MatDrawer; public isHandset$: Observable = this.breakpointObserver.observe('(max-width: 599px)').pipe( map((result) => { @@ -48,8 +49,6 @@ export class AppComponent implements OnDestroy { public showProjectSection: boolean = false; - private destroy$: Subject = new Subject(); - public language: string = 'en'; public privacyPolicy!: PrivacyPolicy.AsObject; constructor( @@ -70,6 +69,7 @@ export class AppComponent implements OnDestroy { private activatedRoute: ActivatedRoute, @Inject(DOCUMENT) private document: Document, private posthog: PosthogService, + private readonly destroyRef: DestroyRef, ) { console.log( '%cWait!', @@ -199,42 +199,43 @@ export class AppComponent implements OnDestroy { this.getProjectCount(); - this.authService.activeOrgChanged.pipe(takeUntil(this.destroy$)).subscribe((org) => { + this.authService.activeOrgChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((org) => { if (org) { this.org = org; this.getProjectCount(); } }); - this.activatedRoute.queryParams.pipe(filter((params) => !!params['org'])).subscribe((params) => { - const { org } = params; - this.authService.getActiveOrg(org); - }); + this.activatedRoute.queryParamMap + .pipe( + map((params) => params.get('org')), + filter(Boolean), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((org) => this.authService.getActiveOrg(org)); - this.authenticationService.authenticationChanged.pipe(takeUntil(this.destroy$)).subscribe((authenticated) => { - if (authenticated) { - this.authService - .getActiveOrg() - .then(async (org) => { - this.org = org; - // TODO add when console storage is implemented - // this.startIntroWorkflow(); - }) - .catch((error) => { - console.error(error); - this.router.navigate(['/users/me']); - }); - } - }); + this.authenticationService.authenticationChanged + .pipe( + filter(Boolean), + switchMap(() => this.authService.getActiveOrg()), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (org) => (this.org = org), + error: async (err) => { + console.error(err); + return this.router.navigate(['/users/me']); + }, + }); this.isDarkTheme = this.themeService.isDarkTheme; - this.isDarkTheme.pipe(takeUntil(this.destroy$)).subscribe((dark) => { + this.isDarkTheme.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((dark) => { const theme = dark ? 'dark-theme' : 'light-theme'; this.onSetTheme(theme); this.setFavicon(theme); }); - this.translate.onLangChange.pipe(takeUntil(this.destroy$)).subscribe((language: LangChangeEvent) => { + this.translate.onLangChange.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((language: LangChangeEvent) => { this.document.documentElement.lang = language.lang; this.language = language.lang; }); @@ -254,11 +255,6 @@ export class AppComponent implements OnDestroy { // }, 1000); // } - public ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - public prepareRoute(outlet: RouterOutlet): boolean { return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation']; } @@ -275,7 +271,7 @@ export class AppComponent implements OnDestroy { const currentUrl = this.router.url; this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { // We use navigateByUrl as our urls may have queryParams - this.router.navigateByUrl(currentUrl); + this.router.navigateByUrl(currentUrl).then(); }); } @@ -283,18 +279,16 @@ export class AppComponent implements OnDestroy { this.translate.addLangs(supportedLanguages); this.translate.setDefaultLang(fallbackLanguage); - this.authService.userSubject.pipe(takeUntil(this.destroy$)).subscribe((userprofile) => { - if (userprofile) { - const cropped = navigator.language.split('-')[0] ?? fallbackLanguage; - const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage; + this.authService.user.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef)).subscribe((userprofile) => { + const cropped = navigator.language.split('-')[0] ?? fallbackLanguage; + const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage; - const lang = userprofile?.human?.profile?.preferredLanguage.match(supportedLanguagesRegexp) - ? userprofile.human.profile?.preferredLanguage - : fallbackLang; - this.translate.use(lang); - this.language = lang; - this.document.documentElement.lang = lang; - } + const lang = userprofile?.human?.profile?.preferredLanguage.match(supportedLanguagesRegexp) + ? userprofile.human.profile?.preferredLanguage + : fallbackLang; + this.translate.use(lang); + this.language = lang; + this.document.documentElement.lang = lang; }); } @@ -308,7 +302,7 @@ export class AppComponent implements OnDestroy { } private setFavicon(theme: string): void { - this.authService.labelpolicy.pipe(takeUntil(this.destroy$)).subscribe((lP) => { + this.authService.labelpolicy$.pipe(startWith(undefined), takeUntilDestroyed(this.destroyRef)).subscribe((lP) => { if (theme === 'dark-theme' && lP?.iconUrlDark) { // Check if asset url is stable, maybe it was deleted but still wasn't applied fetch(lP.iconUrlDark).then((response) => { diff --git a/console/src/app/app.module.ts b/console/src/app/app.module.ts index 96ae614c9e..d6e7e60bea 100644 --- a/console/src/app/app.module.ts +++ b/console/src/app/app.module.ts @@ -18,6 +18,7 @@ import localeNl from '@angular/common/locales/nl'; import localeSv from '@angular/common/locales/sv'; import localeHu from '@angular/common/locales/hu'; import localeKo from '@angular/common/locales/ko'; +import localeRo from '@angular/common/locales/ro'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { MatNativeDateModule } from '@angular/material/core'; import { MatDialogModule } from '@angular/material/dialog'; @@ -32,9 +33,6 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; import * as i18nIsoCountries from 'i18n-iso-countries'; import { from, Observable } from 'rxjs'; -import { AuthGuard } from 'src/app/guards/auth.guard'; -import { RoleGuard } from 'src/app/guards/role.guard'; -import { UserGuard } from 'src/app/guards/user.guard'; import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module'; import { AssetService } from 'src/app/services/asset.service'; import { AppRoutingModule } from './app-routing.module'; @@ -112,6 +110,8 @@ registerLocaleData(localeHu); i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/hu.json')); registerLocaleData(localeKo); i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/ko.json')); +registerLocaleData(localeRo); +i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/ro.json')); export class WebpackTranslateLoader implements TranslateLoader { getTranslation(lang: string): Observable { @@ -170,9 +170,6 @@ const authConfig: AuthConfig = { ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }), ], providers: [ - AuthGuard, - RoleGuard, - UserGuard, ThemeService, EnvironmentService, ExhaustedService, diff --git a/console/src/app/components/feature-toggle/feature-toggle.component.html b/console/src/app/components/feature-toggle/feature-toggle.component.html new file mode 100644 index 0000000000..cb97f1b746 --- /dev/null +++ b/console/src/app/components/feature-toggle/feature-toggle.component.html @@ -0,0 +1,40 @@ +
+ {{ 'SETTING.FEATURES.' + (toggleStateKey | uppercase) | translate }} +
+ + +
+ {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} + +
+
+
+ +
+ {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} +
+
+
+
+
+ + {{ i18nDescription }} +
diff --git a/console/src/app/components/feature-toggle/feature-toggle.component.scss b/console/src/app/components/feature-toggle/feature-toggle.component.scss new file mode 100644 index 0000000000..b1e7c1a3f1 --- /dev/null +++ b/console/src/app/components/feature-toggle/feature-toggle.component.scss @@ -0,0 +1,39 @@ +.feature-row { + display: flex; + flex-direction: column; + padding-bottom: 1rem; + + .row { + display: flex; + align-items: center; + justify-content: space-between; + + .buttongroup { + margin-right: 0.5rem; + margin-top: 0.5rem; + + .toggle-row { + display: flex; + align-items: center; + + i { + margin-right: 0.5rem; + } + + .info-i { + font-size: 1.2rem; + margin-left: 0.5rem; + margin-right: 0; + } + + .current-dot { + height: 8px; + width: 8px; + border-radius: 50%; + margin-left: 0.5rem; + background-color: rgb(59, 128, 247); + } + } + } + } +} diff --git a/console/src/app/components/feature-toggle/feature-toggle.component.ts b/console/src/app/components/feature-toggle/feature-toggle.component.ts new file mode 100644 index 0000000000..fab0b31d48 --- /dev/null +++ b/console/src/app/components/feature-toggle/feature-toggle.component.ts @@ -0,0 +1,45 @@ +import { AsyncPipe, NgIf, UpperCasePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; +import { ToggleStateKeys, ToggleStates } from '../features/features.component'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { FormsModule } from '@angular/forms'; +import { Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb'; +import { ReplaySubject } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Component({ + standalone: true, + selector: 'cnsl-feature-toggle', + templateUrl: './feature-toggle.component.html', + styleUrls: ['./feature-toggle.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatButtonToggleModule, + UpperCasePipe, + TranslateModule, + FormsModule, + MatTooltipModule, + InfoSectionModule, + AsyncPipe, + NgIf, + ], +}) +export class FeatureToggleComponent { + @Input({ required: true }) toggleStateKey!: TKey; + @Input({ required: true }) + set toggleState(toggleState: TValue) { + // we copy the toggleState so we can mutate it + this.toggleState$.next(structuredClone(toggleState)); + } + + @Output() readonly toggleChange = new EventEmitter(); + + protected readonly Source = Source; + protected readonly toggleState$ = new ReplaySubject(1); + protected readonly isInherited$ = this.toggleState$.pipe( + map(({ source }) => source == Source.SYSTEM || source == Source.UNSPECIFIED), + ); +} diff --git a/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.html b/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.html new file mode 100644 index 0000000000..c1feeb894c --- /dev/null +++ b/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.html @@ -0,0 +1,27 @@ + + + {{ 'SETTING.FEATURES.LOGINV2_BASEURI' | translate }} + + + + diff --git a/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.ts b/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.ts new file mode 100644 index 0000000000..01648d22ad --- /dev/null +++ b/console/src/app/components/feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component.ts @@ -0,0 +1,47 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, Input, Output } from '@angular/core'; +import { FeatureToggleComponent } from '../feature-toggle.component'; +import { ToggleStates } from 'src/app/components/features/features.component'; +import { distinctUntilKeyChanged, ReplaySubject } from 'rxjs'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { AsyncPipe, NgIf } from '@angular/common'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { InputModule } from 'src/app/modules/input/input.module'; +import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; +import { MatButtonModule } from '@angular/material/button'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; + +@Component({ + standalone: true, + selector: 'cnsl-login-v2-feature-toggle', + templateUrl: './login-v2-feature-toggle.component.html', + imports: [ + FeatureToggleComponent, + AsyncPipe, + NgIf, + ReactiveFormsModule, + InputModule, + HasRolePipeModule, + MatButtonModule, + TranslateModule, + MatTooltipModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoginV2FeatureToggleComponent { + @Input({ required: true }) + set toggleState(toggleState: ToggleStates['loginV2']) { + this.toggleState$.next(toggleState); + } + @Output() + public toggleChanged = new EventEmitter(); + + protected readonly toggleState$ = new ReplaySubject(1); + protected readonly baseUri = new FormControl('', { nonNullable: true, validators: [Validators.required] }); + + constructor(destroyRef: DestroyRef) { + this.toggleState$.pipe(distinctUntilKeyChanged('baseUri'), takeUntilDestroyed(destroyRef)).subscribe(({ baseUri }) => { + this.baseUri.setValue(baseUri); + }); + } +} diff --git a/console/src/app/components/features/features.component.html b/console/src/app/components/features/features.component.html index e663569210..30d8f629af 100644 --- a/console/src/app/components/features/features.component.html +++ b/console/src/app/components/features/features.component.html @@ -13,402 +13,20 @@

{{ 'DESCRIPTIONS.SETTINGS.FEATURES.DESCRIPTION' | translate }}

- - +
-
- {{ 'SETTING.FEATURES.LOGINDEFAULTORG' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- - {{ - 'SETTING.FEATURES.LOGINDEFAULTORG_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.OIDCLEGACYINTROSPECTION' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.OIDCLEGACYINTROSPECTION_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.OIDCTOKENEXCHANGE' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.OIDCTOKENEXCHANGE_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.OIDCTRIGGERINTROSPECTIONPROJECTIONS' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.USERSCHEMA' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.USERSCHEMA_DESCRIPTION' | translate - }} -
- -
- {{ 'SETTING.FEATURES.ACTIONS' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ 'SETTING.FEATURES.ACTIONS_DESCRIPTION' | translate }} -
- -
- {{ 'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION' | translate }} -
- - -
- {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} - -
-
-
-
- -
- {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} -
-
- -
- {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} -
-
-
-
- {{ - 'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION' | translate - }} -
+ +
- - - - {{ 'SETTING.FEATURES.SOURCE.' + source | translate }} - - diff --git a/console/src/app/components/features/features.component.ts b/console/src/app/components/features/features.component.ts index 327e9d2792..d95bbdde43 100644 --- a/console/src/app/components/features/features.component.ts +++ b/console/src/app/components/features/features.component.ts @@ -1,46 +1,56 @@ import { CommonModule } from '@angular/common'; -import { Component, OnDestroy } from '@angular/core'; +import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; -import { BehaviorSubject, Subject } from 'rxjs'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { CardModule } from 'src/app/modules/card/card.module'; -import { DisplayJsonDialogComponent } from 'src/app/modules/display-json-dialog/display-json-dialog.component'; import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; -import { Event } from 'src/app/proto/generated/zitadel/event_pb'; -import { Source } from 'src/app/proto/generated/zitadel/feature/v2beta/feature_pb'; +import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { FeatureToggleComponent } from '../feature-toggle/feature-toggle.component'; +import { NewFeatureService } from 'src/app/services/new-feature.service'; import { GetInstanceFeaturesResponse, - SetInstanceFeaturesRequest, -} from 'src/app/proto/generated/zitadel/feature/v2beta/instance_pb'; -import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; -import { FeatureService } from 'src/app/services/feature.service'; -import { ToastService } from 'src/app/services/toast.service'; + SetInstanceFeaturesRequestSchema, +} from '@zitadel/proto/zitadel/feature/v2/instance_pb'; +import { Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { firstValueFrom, Observable, ReplaySubject, shareReplay, switchMap } from 'rxjs'; +import { filter, map, startWith } from 'rxjs/operators'; +import { LoginV2FeatureToggleComponent } from '../feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component'; -enum ToggleState { - ENABLED = 'ENABLED', - DISABLED = 'DISABLED', - INHERITED = 'INHERITED', -} +// to add a new feature, add the key here and in the FEATURE_KEYS array +const FEATURE_KEYS = [ + 'consoleUseV2UserApi', + 'debugOidcParentError', + 'disableUserTokenEvent', + 'enableBackChannelLogout', + // 'improvedPerformance', + 'loginDefaultOrg', + 'oidcLegacyIntrospection', + 'oidcSingleV1SessionTermination', + 'oidcTokenExchange', + 'oidcTriggerIntrospectionProjections', + 'permissionCheckV2', + 'userSchema', + 'webKey', +] as const; -type FeatureState = { source: Source; state: ToggleState }; -type ToggleStates = { - loginDefaultOrg?: FeatureState; - oidcTriggerIntrospectionProjections?: FeatureState; - oidcLegacyIntrospection?: FeatureState; - userSchema?: FeatureState; - oidcTokenExchange?: FeatureState; - actions?: FeatureState; - oidcSingleV1SessionTermination?: FeatureState; +export type ToggleState = { source: Source; enabled: boolean }; +export type ToggleStates = { + [key in (typeof FEATURE_KEYS)[number]]: ToggleState; +} & { + loginV2: ToggleState & { baseUri: string }; }; +export type ToggleStateKeys = keyof ToggleStates; + @Component({ imports: [ CommonModule, @@ -55,27 +65,24 @@ type ToggleStates = { InfoSectionModule, MatTooltipModule, HasRoleModule, + FeatureToggleComponent, + LoginV2FeatureToggleComponent, ], standalone: true, selector: 'cnsl-features', templateUrl: './features.component.html', styleUrls: ['./features.component.scss'], }) -export class FeaturesComponent implements OnDestroy { - private destroy$: Subject = new Subject(); - - public _loading: BehaviorSubject = new BehaviorSubject(false); - public featureData: GetInstanceFeaturesResponse.AsObject | undefined = undefined; - - public toggleStates: ToggleStates | undefined = undefined; - public Source: any = Source; - public ToggleState: any = ToggleState; +export class FeaturesComponent { + private readonly refresh$ = new ReplaySubject(1); + protected readonly toggleStates$: Observable; + protected readonly Source = Source; + protected readonly FEATURE_KEYS = FEATURE_KEYS; constructor( - private featureService: FeatureService, - private breadcrumbService: BreadcrumbService, - private toast: ToastService, - private dialog: MatDialog, + private readonly featureService: NewFeatureService, + private readonly breadcrumbService: BreadcrumbService, + private readonly toast: ToastService, ) { const breadcrumbs = [ new Breadcrumb({ @@ -86,167 +93,84 @@ export class FeaturesComponent implements OnDestroy { ]; this.breadcrumbService.setBreadcrumb(breadcrumbs); - this.getFeatures(true); + this.toggleStates$ = this.getToggleStates().pipe(shareReplay({ refCount: true, bufferSize: 1 })); } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + private getToggleStates() { + return this.refresh$.pipe( + startWith(true), + switchMap(async () => { + try { + return await this.featureService.getInstanceFeatures(); + } catch (error) { + this.toast.showError(error); + return undefined; + } + }), + filter(Boolean), + map((res) => this.createToggleStates(res)), + ); } - public openDialog(event: Event): void { - this.dialog.open(DisplayJsonDialogComponent, { - data: { - event: event, + private createToggleStates(featureData: GetInstanceFeaturesResponse): ToggleStates { + return FEATURE_KEYS.reduce( + (acc, key) => { + const feature = featureData[key]; + acc[key] = { + source: feature?.source ?? Source.SYSTEM, + enabled: !!feature?.enabled, + }; + return acc; }, - width: '450px', - }); + { + // to add special feature flags they have to be mapped here + loginV2: { + source: featureData.loginV2?.source ?? Source.SYSTEM, + enabled: !!featureData.loginV2?.required, + baseUri: featureData.loginV2?.baseUri ?? '', + }, + } as ToggleStates, + ); } - public validateAndSave() { - this.featureService.resetInstanceFeatures().then(() => { - const req = new SetInstanceFeaturesRequest(); - let changed = false; + public async saveFeatures(key: TKey, value: TValue) { + const toggleStates = { ...(await firstValueFrom(this.toggleStates$)), [key]: value }; - console.log(this.toggleStates); + const req = FEATURE_KEYS.reduce>((acc, key) => { + acc[key] = toggleStates[key].enabled; + return acc; + }, {}); - if (this.toggleStates?.loginDefaultOrg?.state !== ToggleState.INHERITED) { - req.setLoginDefaultOrg(this.toggleStates?.loginDefaultOrg?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.oidcTriggerIntrospectionProjections?.state !== ToggleState.INHERITED) { - req.setOidcTriggerIntrospectionProjections( - this.toggleStates?.oidcTriggerIntrospectionProjections?.state === ToggleState.ENABLED, - ); - changed = true; - } - if (this.toggleStates?.oidcLegacyIntrospection?.state !== ToggleState.INHERITED) { - req.setOidcLegacyIntrospection(this.toggleStates?.oidcLegacyIntrospection?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.userSchema?.state !== ToggleState.INHERITED) { - req.setUserSchema(this.toggleStates?.userSchema?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.oidcTokenExchange?.state !== ToggleState.INHERITED) { - req.setOidcTokenExchange(this.toggleStates?.oidcTokenExchange?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.actions?.state !== ToggleState.INHERITED) { - req.setActions(this.toggleStates?.actions?.state === ToggleState.ENABLED); - changed = true; - } - if (this.toggleStates?.oidcSingleV1SessionTermination?.state !== ToggleState.INHERITED) { - req.setOidcSingleV1SessionTermination( - this.toggleStates?.oidcSingleV1SessionTermination?.state === ToggleState.ENABLED, - ); - changed = true; - } + // to save special flags they have to be handled here + req.loginV2 = { + required: toggleStates.loginV2.enabled, + baseUri: toggleStates.loginV2.baseUri, + }; - if (changed) { - this.featureService - .setInstanceFeatures(req) - .then(() => { - this.toast.showInfo('POLICY.TOAST.SET', true); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - }); + try { + await this.featureService.setInstanceFeatures(req); + + // needed because of eventual consistency + await new Promise((res) => setTimeout(res, 1000)); + this.refresh$.next(true); + + this.toast.showInfo('POLICY.TOAST.SET', true); + } catch (error) { + this.toast.showError(error); + } } - private getFeatures(inheritance: boolean) { - this.featureService.getInstanceFeatures(inheritance).then((instanceFeaturesResponse) => { - this.featureData = instanceFeaturesResponse.toObject(); - console.log(this.featureData); + public async resetFeatures() { + try { + await this.featureService.resetInstanceFeatures(); - this.toggleStates = { - loginDefaultOrg: { - source: this.featureData.loginDefaultOrg?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.loginDefaultOrg?.source === Source.SOURCE_SYSTEM || - this.featureData.loginDefaultOrg?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.loginDefaultOrg?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - oidcTriggerIntrospectionProjections: { - source: this.featureData.oidcTriggerIntrospectionProjections?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_SYSTEM || - this.featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.oidcTriggerIntrospectionProjections?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - oidcLegacyIntrospection: { - source: this.featureData.oidcLegacyIntrospection?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.oidcLegacyIntrospection?.source === Source.SOURCE_SYSTEM || - this.featureData.oidcLegacyIntrospection?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.oidcLegacyIntrospection?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - userSchema: { - source: this.featureData.userSchema?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.userSchema?.source === Source.SOURCE_SYSTEM || - this.featureData.userSchema?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.userSchema?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - oidcTokenExchange: { - source: this.featureData.oidcTokenExchange?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.oidcTokenExchange?.source === Source.SOURCE_SYSTEM || - this.featureData.oidcTokenExchange?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.oidcTokenExchange?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - actions: { - source: Source.SOURCE_SYSTEM, - state: - this.featureData.actions?.source === Source.SOURCE_SYSTEM || - this.featureData.actions?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.actions?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - oidcSingleV1SessionTermination: { - source: this.featureData.oidcSingleV1SessionTermination?.source || Source.SOURCE_SYSTEM, - state: - this.featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_SYSTEM || - this.featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_UNSPECIFIED - ? ToggleState.INHERITED - : !!this.featureData.oidcSingleV1SessionTermination?.enabled - ? ToggleState.ENABLED - : ToggleState.DISABLED, - }, - }; - }); - } + // needed because of eventual consistency + await new Promise((res) => setTimeout(res, 1000)); + this.refresh$.next(true); - public resetSettings(): void { - this.featureService - .resetInstanceFeatures() - .then(() => { - this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true); - setTimeout(() => { - this.getFeatures(true); - }, 1000); - }) - .catch((error) => { - this.toast.showError(error); - }); + this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true); + } catch (error) { + this.toast.showError(error); + } } } diff --git a/console/src/app/components/framework-change/framework-change.component.ts b/console/src/app/components/framework-change/framework-change.component.ts index 4efaf42761..03e5557fc1 100644 --- a/console/src/app/components/framework-change/framework-change.component.ts +++ b/console/src/app/components/framework-change/framework-change.component.ts @@ -1,23 +1,12 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, OnDestroy, OnInit, Output, effect, signal } from '@angular/core'; +import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { ActivatedRoute, Params, RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import frameworkDefinition from '../../../../../docs/frameworks.json'; import { MatButtonModule } from '@angular/material/button'; -import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info'; -import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames'; -import { FrameworkAutocompleteComponent } from '../framework-autocomplete/framework-autocomplete.component'; import { Framework } from '../quickstart/quickstart.component'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { - MatDialog, - MatDialogActions, - MatDialogClose, - MatDialogContent, - MatDialogModule, - MatDialogRef, - MatDialogTitle, -} from '@angular/material/dialog'; +import { MatDialog } from '@angular/material/dialog'; import { FrameworkChangeDialogComponent } from './framework-change-dialog.component'; @Component({ @@ -25,12 +14,11 @@ import { FrameworkChangeDialogComponent } from './framework-change-dialog.compon selector: 'cnsl-framework-change', templateUrl: './framework-change.component.html', styleUrls: ['./framework-change.component.scss'], - imports: [TranslateModule, RouterModule, CommonModule, MatButtonModule, FrameworkAutocompleteComponent], + imports: [TranslateModule, RouterModule, CommonModule, MatButtonModule], }) export class FrameworkChangeComponent implements OnInit, OnDestroy { private destroy$: Subject = new Subject(); public framework: BehaviorSubject = new BehaviorSubject(undefined); - public showFrameworkAutocomplete = signal(false); @Output() public frameworkChanged: EventEmitter = new EventEmitter(); public frameworks: Framework[] = frameworkDefinition.map((f) => { return { diff --git a/console/src/app/components/oidc-configuration/oidc-configuration.component.ts b/console/src/app/components/oidc-configuration/oidc-configuration.component.ts index c020bd1caa..ac2afaf4b9 100644 --- a/console/src/app/components/oidc-configuration/oidc-configuration.component.ts +++ b/console/src/app/components/oidc-configuration/oidc-configuration.component.ts @@ -2,10 +2,8 @@ import { CommonModule } from '@angular/common'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import frameworkDefinition from '../../../../../docs/frameworks.json'; import { MatButtonModule } from '@angular/material/button'; -import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info'; -import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames'; +import type { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames'; import { AddOIDCAppRequest } from 'src/app/proto/generated/zitadel/management_pb'; export type FrameworkDefinition = { diff --git a/console/src/app/components/quickstart/quickstart.component.ts b/console/src/app/components/quickstart/quickstart.component.ts index fcac992562..1296d62579 100644 --- a/console/src/app/components/quickstart/quickstart.component.ts +++ b/console/src/app/components/quickstart/quickstart.component.ts @@ -4,8 +4,7 @@ import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import frameworkDefinition from '../../../../../docs/frameworks.json'; import { MatButtonModule } from '@angular/material/button'; -import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info'; -import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames'; +import type { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames'; import { OIDC_CONFIGURATIONS } from 'src/app/utils/framework'; export type FrameworkDefinition = { diff --git a/console/src/app/directives/has-role/has-role.directive.ts b/console/src/app/directives/has-role/has-role.directive.ts index b58e1f3a10..9ba21c1dd2 100644 --- a/console/src/app/directives/has-role/has-role.directive.ts +++ b/console/src/app/directives/has-role/has-role.directive.ts @@ -1,18 +1,17 @@ -import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core'; -import { Subject, takeUntil } from 'rxjs'; +import { DestroyRef, Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Directive({ selector: '[cnslHasRole]', }) -export class HasRoleDirective implements OnDestroy { - private destroy$: Subject = new Subject(); +export class HasRoleDirective { private hasView: boolean = false; @Input() public set hasRole(roles: string[] | RegExp[] | undefined) { if (roles && roles.length > 0) { this.authService .isAllowed(roles) - .pipe(takeUntil(this.destroy$)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((isAllowed) => { if (isAllowed && !this.hasView) { if (this.viewContainerRef.length !== 0) { @@ -38,10 +37,6 @@ export class HasRoleDirective implements OnDestroy { private authService: GrpcAuthService, protected templateRef: TemplateRef, protected viewContainerRef: ViewContainerRef, + private readonly destroyRef: DestroyRef, ) {} - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.directive.ts b/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.directive.ts new file mode 100644 index 0000000000..a3a145964b --- /dev/null +++ b/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.directive.ts @@ -0,0 +1,16 @@ +import { Directive, Input } from '@angular/core'; +import { DataSource } from '@angular/cdk/collections'; +import { MatCellDef } from '@angular/material/table'; +import { CdkCellDef } from '@angular/cdk/table'; + +@Directive({ + selector: '[cnslCellDef]', + providers: [{ provide: CdkCellDef, useExisting: TypeSafeCellDefDirective }], +}) +export class TypeSafeCellDefDirective extends MatCellDef { + @Input({ required: true }) cnslCellDefDataSource!: DataSource; + + static ngTemplateContextGuard(_dir: TypeSafeCellDefDirective, _ctx: any): _ctx is { $implicit: T; index: number } { + return true; + } +} diff --git a/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.module.ts b/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.module.ts new file mode 100644 index 0000000000..01557ed65c --- /dev/null +++ b/console/src/app/directives/type-safe-cell-def/type-safe-cell-def.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { TypeSafeCellDefDirective } from './type-safe-cell-def.directive'; + +@NgModule({ + declarations: [TypeSafeCellDefDirective], + imports: [CommonModule], + exports: [TypeSafeCellDefDirective], +}) +export class TypeSafeCellDefModule {} diff --git a/console/src/app/guards/auth.guard.ts b/console/src/app/guards/auth.guard.ts index ca996c3312..8f1ebaabde 100644 --- a/console/src/app/guards/auth.guard.ts +++ b/console/src/app/guards/auth.guard.ts @@ -1,34 +1,26 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; import { AuthConfig } from 'angular-oauth2-oidc'; -import { Observable } from 'rxjs'; import { AuthenticationService } from '../services/authentication.service'; -@Injectable({ - providedIn: 'root', -}) -export class AuthGuard { - constructor(private auth: AuthenticationService) {} +export const authGuard: CanActivateFn = (route) => { + const auth = inject(AuthenticationService); - public canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable | Promise | Promise | boolean { - if (!this.auth.authenticated) { - if (route.queryParams && route.queryParams['login_hint']) { - const hint = route.queryParams['login_hint']; - const configWithPrompt: Partial = { - customQueryParams: { - login_hint: hint, - }, - }; - console.log(`authenticate with login_hint: ${hint}`); - this.auth.authenticate(configWithPrompt); - } else { - return this.auth.authenticate(); - } + if (!auth.authenticated) { + if (route.queryParams && route.queryParams['login_hint']) { + const hint = route.queryParams['login_hint']; + const configWithPrompt: Partial = { + customQueryParams: { + login_hint: hint, + }, + }; + console.log(`authenticate with login_hint: ${hint}`); + auth.authenticate(configWithPrompt).then(); + } else { + return auth.authenticate(); } - return this.auth.authenticated; } -} + + return auth.authenticated; +}; diff --git a/console/src/app/guards/role-guard.ts b/console/src/app/guards/role-guard.ts new file mode 100644 index 0000000000..887b9e8802 --- /dev/null +++ b/console/src/app/guards/role-guard.ts @@ -0,0 +1,9 @@ +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; + +import { GrpcAuthService } from '../services/grpc-auth.service'; + +export const roleGuard: CanActivateFn = (route) => { + const authService = inject(GrpcAuthService); + return authService.isAllowed(route.data['roles'], route.data['requiresAll']); +}; diff --git a/console/src/app/guards/role.guard.ts b/console/src/app/guards/role.guard.ts deleted file mode 100644 index 951ffa1b60..0000000000 --- a/console/src/app/guards/role.guard.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; - -import { GrpcAuthService } from '../services/grpc-auth.service'; - -@Injectable({ - providedIn: 'root', -}) -export class RoleGuard { - constructor(private authService: GrpcAuthService) {} - - public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.authService.isAllowed(route.data['roles'], route.data['requiresAll']); - } -} diff --git a/console/src/app/guards/user-guard.ts b/console/src/app/guards/user-guard.ts new file mode 100644 index 0000000000..fc97cf5a2e --- /dev/null +++ b/console/src/app/guards/user-guard.ts @@ -0,0 +1,21 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { map, take } from 'rxjs'; + +import { GrpcAuthService } from '../services/grpc-auth.service'; + +export const userGuard: CanActivateFn = (route) => { + const authService = inject(GrpcAuthService); + const router = inject(Router); + + return authService.user.pipe( + take(1), + map((user) => { + const isMe = user?.id === route.params['id']; + if (isMe) { + router.navigate(['/users', 'me']).then(); + } + return !isMe; + }), + ); +}; diff --git a/console/src/app/guards/user.guard.ts b/console/src/app/guards/user.guard.ts deleted file mode 100644 index b4527753fe..0000000000 --- a/console/src/app/guards/user.guard.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { map, Observable, take } from 'rxjs'; - -import { GrpcAuthService } from '../services/grpc-auth.service'; - -@Injectable({ - providedIn: 'root', -}) -export class UserGuard { - constructor( - private authService: GrpcAuthService, - private router: Router, - ) {} - - public canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable | Promise | boolean { - return this.authService.user.pipe( - take(1), - map((user) => { - const isMe = user?.id === route.params['id']; - if (isMe) { - this.router.navigate(['/users', 'me']); - } - return !isMe; - }), - ); - } -} diff --git a/console/src/app/modules/accounts-card/accounts-card.component.html b/console/src/app/modules/accounts-card/accounts-card.component.html index 1ed1649545..24fe098331 100644 --- a/console/src/app/modules/accounts-card/accounts-card.component.html +++ b/console/src/app/modules/accounts-card/accounts-card.component.html @@ -1,7 +1,6 @@ -
+
{{ 'USER.EDITACCOUNT' | translate }} diff --git a/console/src/app/modules/accounts-card/accounts-card.component.ts b/console/src/app/modules/accounts-card/accounts-card.component.ts index 617a41bf6d..273af86467 100644 --- a/console/src/app/modules/accounts-card/accounts-card.component.ts +++ b/console/src/app/modules/accounts-card/accounts-card.component.ts @@ -1,61 +1,158 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, NgIterable, Output } from '@angular/core'; import { Router } from '@angular/router'; import { AuthConfig } from 'angular-oauth2-oidc'; -import { Session, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; +import { SessionState as V1SessionState, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { AuthenticationService } from 'src/app/services/authentication.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { SessionService } from 'src/app/services/session.service'; +import { + catchError, + defer, + from, + map, + mergeMap, + Observable, + of, + ReplaySubject, + shareReplay, + switchMap, + timeout, + TimeoutError, + toArray, +} from 'rxjs'; +import { NewFeatureService } from 'src/app/services/new-feature.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { SessionState as V2SessionState } from '@zitadel/proto/zitadel/user_pb'; +import { filter, withLatestFrom } from 'rxjs/operators'; + +interface V1AndV2Session { + displayName: string; + avatarUrl: string; + loginName: string; + userName: string; + authState: V1SessionState | V2SessionState; +} @Component({ selector: 'cnsl-accounts-card', templateUrl: './accounts-card.component.html', styleUrls: ['./accounts-card.component.scss'], }) -export class AccountsCardComponent implements OnInit { - @Input() public user?: User.AsObject; - @Input() public iamuser: boolean | null = false; - - @Output() public closedCard: EventEmitter = new EventEmitter(); - public sessions: Session.AsObject[] = []; - public loadingUsers: boolean = false; - public UserState: any = UserState; - constructor( - public authService: AuthenticationService, - private router: Router, - private userService: GrpcAuthService, - ) { - this.userService - .listMyUserSessions() - .then((sessions) => { - this.sessions = sessions.resultList.filter((user) => user.loginName !== this.user?.preferredLoginName); - this.loadingUsers = false; - }) - .catch(() => { - this.loadingUsers = false; - }); +export class AccountsCardComponent { + @Input({ required: true }) + public set user(user: User.AsObject) { + this.user$.next(user); } - ngOnInit(): void { - this.loadingUsers = true; + @Input() public iamuser: boolean | null = false; + + @Output() public closedCard = new EventEmitter(); + + protected readonly user$ = new ReplaySubject(1); + protected readonly UserState = UserState; + private readonly labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined }); + protected readonly sessions$: Observable; + + constructor( + protected readonly authService: AuthenticationService, + private readonly router: Router, + private readonly userService: GrpcAuthService, + private readonly sessionService: SessionService, + private readonly featureService: NewFeatureService, + private readonly toast: ToastService, + ) { + this.sessions$ = this.getSessions().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + } + + private getUseLoginV2() { + return defer(() => this.featureService.getInstanceFeatures()).pipe( + map(({ loginV2 }) => loginV2?.required ?? false), + timeout(1000), + catchError((err) => { + if (!(err instanceof TimeoutError)) { + this.toast.showError(err); + } + return of(false); + }), + ); + } + + private getSessions(): Observable { + const useLoginV2$ = this.getUseLoginV2(); + + return useLoginV2$.pipe( + switchMap((useLoginV2) => { + if (useLoginV2) { + return this.getV2Sessions(); + } else { + return this.getV1Sessions(); + } + }), + catchError((err) => { + this.toast.showError(err); + return of([]); + }), + ); + } + + private getV1Sessions(): Observable { + return defer(() => this.userService.listMyUserSessions()).pipe( + mergeMap(({ resultList }) => from(resultList)), + withLatestFrom(this.user$), + filter(([{ loginName }, user]) => loginName !== user.preferredLoginName), + map(([s]) => ({ + displayName: s.displayName, + avatarUrl: s.avatarUrl, + loginName: s.loginName, + authState: s.authState, + userName: s.userName, + })), + toArray(), + ); + } + + private getV2Sessions(): Observable { + return defer(() => + this.sessionService.listSessions({ + queries: [ + { + query: { + case: 'userAgentQuery', + value: {}, + }, + }, + ], + }), + ).pipe( + mergeMap(({ sessions }) => from(sessions)), + withLatestFrom(this.user$), + filter(([s, user]) => s.factors?.user?.loginName !== user.preferredLoginName), + map(([s]) => ({ + displayName: s.factors?.user?.displayName ?? '', + avatarUrl: '', + loginName: s.factors?.user?.loginName ?? '', + authState: V2SessionState.ACTIVE, + userName: s.factors?.user?.loginName ?? '', + })), + map((s) => [s.loginName, s] as const), + toArray(), + map((sessions) => Array.from(new Map(sessions).values())), // Ensure unique loginNames + ); } public editUserProfile(): void { - this.router.navigate(['users/me']); + this.router.navigate(['users/me']).then(); this.closedCard.emit(); } - public closeCard(element: HTMLElement): void { - if (!element.classList.contains('dontcloseonclick')) { - this.closedCard.emit(); - } - } - public selectAccount(loginHint: string): void { const configWithPrompt: Partial = { customQueryParams: { login_hint: loginHint, }, }; - this.authService.authenticate(configWithPrompt); + this.authService.authenticate(configWithPrompt).then(); } public selectNewAccount(): void { @@ -64,11 +161,11 @@ export class AccountsCardComponent implements OnInit { prompt: 'login', } as any, }; - this.authService.authenticate(configWithPrompt); + this.authService.authenticate(configWithPrompt).then(); } public logout(): void { - const lP = JSON.stringify(this.userService.labelpolicy.getValue()); + const lP = JSON.stringify(this.labelpolicy()); localStorage.setItem('labelPolicyOnSignout', lP); this.authService.signout(); diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html new file mode 100644 index 0000000000..82f04fb124 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html @@ -0,0 +1,69 @@ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'ACTIONSTWO.EXECUTION.TABLE.CONDITION' | translate }} + + {{ row.execution.condition | condition }} + + {{ 'ACTIONSTWO.EXECUTION.TABLE.TYPE' | translate }} + {{ 'ACTIONSTWO.EXECUTION.TYPES.' + row.execution.condition.conditionType.case | translate }} + {{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }} +
+ {{ target.name }} + +
+
+ {{ 'ACTIONSTWO.EXECUTION.TABLE.CREATIONDATE' | translate }} + + {{ row.execution.creationDate | timestampToDate | localizedDate: 'regular' }} + + + + +
+
+
diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.scss b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.scss new file mode 100644 index 0000000000..041332a88b --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.scss @@ -0,0 +1,12 @@ +.target-key { + display: flex; + white-space: nowrap; +} + +.icon { + font-size: 14px; + height: 14px; + width: 14px; + margin-right: 0.5rem; + margin-left: -0.5rem; +} diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts new file mode 100644 index 0000000000..2d9942c406 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts @@ -0,0 +1,107 @@ +import { ChangeDetectionStrategy, Component, computed, effect, EventEmitter, Input, Output } from '@angular/core'; +import { combineLatestWith, Observable, ReplaySubject } from 'rxjs'; +import { filter, map, startWith } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { CorrectlyTypedExecution } from '../../actions-two-add-action/actions-two-add-action-dialog.component'; + +@Component({ + selector: 'cnsl-actions-two-actions-table', + templateUrl: './actions-two-actions-table.component.html', + styleUrls: ['./actions-two-actions-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ActionsTwoActionsTableComponent { + @Output() + public readonly refresh = new EventEmitter(); + + @Output() + public readonly selected = new EventEmitter(); + + @Output() + public readonly delete = new EventEmitter(); + + @Input({ required: true }) + public set executions(executions: CorrectlyTypedExecution[] | null) { + this.executions$.next(executions); + } + + @Input({ required: true }) + public set targets(targets: Target[] | null) { + this.targets$.next(targets); + } + + private readonly executions$ = new ReplaySubject(1); + + private readonly targets$ = new ReplaySubject(1); + + protected readonly dataSource = this.getDataSource(); + + protected readonly loading = this.getLoading(); + + private getDataSource() { + const executions$: Observable = this.executions$.pipe(filter(Boolean), startWith([])); + const executionsSignal = toSignal(executions$, { requireSync: true }); + + const targetsMapSignal = this.getTargetsMap(); + + const dataSignal = computed(() => { + const executions = executionsSignal(); + const targetsMap = targetsMapSignal(); + + if (targetsMap.size === 0) { + return []; + } + + return executions.map((execution) => { + const mappedTargets = execution.targets.map((target) => { + const targetType = targetsMap.get(target.type.value); + if (!targetType) { + throw new Error(`Target with id ${target.type.value} not found`); + } + return targetType; + }); + return { execution, mappedTargets }; + }); + }); + + const dataSource = new MatTableDataSource(dataSignal()); + + effect(() => { + const data = dataSignal(); + if (dataSource.data !== data) { + dataSource.data = data; + } + }); + + return dataSource; + } + + private getTargetsMap() { + const targets$ = this.targets$.pipe(filter(Boolean), startWith([] as Target[])); + const targetsSignal = toSignal(targets$, { requireSync: true }); + + return computed(() => { + const map = new Map(); + for (const target of targetsSignal()) { + map.set(target.id, target); + } + return map; + }); + } + + private getLoading() { + const loading$ = this.executions$.pipe( + combineLatestWith(this.targets$), + map(([executions, targets]) => executions === null || targets === null), + startWith(true), + ); + + return toSignal(loading$, { requireSync: true }); + } + + protected trackTarget(_: number, target: Target) { + return target.id; + } +} diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html new file mode 100644 index 0000000000..c22b03ef76 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html @@ -0,0 +1,14 @@ +

{{ 'ACTIONSTWO.EXECUTION.TITLE' | translate }}

+

{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}

+ + + + diff --git a/cmd/initialise/sql/postgres/11_settings.sql b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.scss similarity index 100% rename from cmd/initialise/sql/postgres/11_settings.sql rename to console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.scss diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts new file mode 100644 index 0000000000..c709eff079 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts @@ -0,0 +1,113 @@ +import { ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core'; +import { ActionService } from 'src/app/services/action.service'; +import { lastValueFrom, Observable, of, Subject } from 'rxjs'; +import { catchError, map, startWith, switchMap } from 'rxjs/operators'; +import { ToastService } from 'src/app/services/toast.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + ActionTwoAddActionDialogComponent, + ActionTwoAddActionDialogData, + ActionTwoAddActionDialogResult, + CorrectlyTypedExecution, + correctlyTypeExecution, +} from '../actions-two-add-action/actions-two-add-action-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; + +@Component({ + selector: 'cnsl-actions-two-actions', + templateUrl: './actions-two-actions.component.html', + styleUrls: ['./actions-two-actions.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ActionsTwoActionsComponent { + protected readonly refresh$ = new Subject(); + protected readonly executions$: Observable; + protected readonly targets$: Observable; + + constructor( + private readonly actionService: ActionService, + private readonly toast: ToastService, + private readonly destroyRef: DestroyRef, + private readonly dialog: MatDialog, + ) { + this.executions$ = this.getExecutions$(); + this.targets$ = this.getTargets$(); + } + + private getExecutions$() { + return this.refresh$.pipe( + startWith(true), + switchMap(() => { + return this.actionService.listExecutions({}); + }), + map(({ result }) => result.map(correctlyTypeExecution)), + catchError((err) => { + this.toast.showError(err); + return of([]); + }), + ); + } + + private getTargets$() { + return this.refresh$.pipe( + startWith(true), + switchMap(() => { + return this.actionService.listTargets({}); + }), + map(({ result }) => result), + catchError((err) => { + this.toast.showError(err); + return of([]); + }), + ); + } + + public async openDialog(execution?: CorrectlyTypedExecution): Promise { + const request$ = this.dialog + .open( + ActionTwoAddActionDialogComponent, + { + width: '400px', + data: execution + ? { + execution, + } + : {}, + }, + ) + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)); + + const request = await lastValueFrom(request$); + if (!request) { + return; + } + + try { + await this.actionService.setExecution(request); + await new Promise((res) => setTimeout(res, 1000)); + this.refresh$.next(true); + } catch (error) { + console.error(error); + this.toast.showError(error); + } + } + + public async deleteExecution(execution: CorrectlyTypedExecution) { + const deleteReq: MessageInitShape = { + condition: execution.condition, + targets: [], + }; + try { + await this.actionService.setExecution(deleteReq); + await new Promise((res) => setTimeout(res, 1000)); + this.refresh$.next(true); + } catch (error) { + console.error(error); + this.toast.showError(error); + } + } +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html new file mode 100644 index 0000000000..401e5e521d --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html @@ -0,0 +1,116 @@ +
+ +

{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.REQ_RESP_DESCRIPTION' | translate }}

+ +
+ +
+ {{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }} + {{ + 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate + }} +
+
+
+ +

{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_SERVICE.TITLE' | translate }}

+ + + {{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_SERVICE.DESCRIPTION' | translate }} + + + + + + + + {{ service }} + + + + +

{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_METHOD.TITLE' | translate }}

+ + + {{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_METHOD.DESCRIPTION' | translate }} + + + + + + + + {{ method }} + + + +
+ + + + {{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.FUNCTIONNAME.TITLE' | translate }} + + + + + + + + {{ function }} + + + + {{ + 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.FUNCTIONNAME.DESCRIPTION' | translate + }} + + + + +
+ +
+ {{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }} + {{ + 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate + }} +
+
+
+ +

{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_GROUP.TITLE' | translate }}

+ + + {{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_GROUP.DESCRIPTION' | translate }} + + + +

{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_EVENT.TITLE' | translate }}

+ + + {{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_EVENT.DESCRIPTION' | translate }} + + +
+ +
+ + +
+
diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.scss b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.scss new file mode 100644 index 0000000000..0cff2adc73 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.scss @@ -0,0 +1,21 @@ +.execution-condition-text { + display: flex; + flex-direction: column; + + .description { + font-size: 0.9rem; + } +} + +.condition-description { + margin-bottom: 0; +} + +.name-hint { + font-size: 12px; +} + +.actions { + display: flex; + justify-content: space-between; +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.spec.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.spec.ts new file mode 100644 index 0000000000..982df301fd --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.spec.ts @@ -0,0 +1,20 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActionsTwoAddActionConditionComponent } from './actions-two-add-action-condition.component'; + +describe('ActionsTwoAddActionConditionComponent', () => { + let component: ActionsTwoAddActionConditionComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ActionsTwoAddActionConditionComponent], + }); + fixture = TestBed.createComponent(ActionsTwoAddActionConditionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.ts new file mode 100644 index 0000000000..4508f31230 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.ts @@ -0,0 +1,343 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { InputModule } from 'src/app/modules/input/input.module'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + ValidatorFn, +} from '@angular/forms'; +import { + Observable, + catchError, + defer, + map, + of, + shareReplay, + ReplaySubject, + ObservedValueOf, + switchMap, + combineLatestWith, + OperatorFunction, +} from 'rxjs'; +import { MatRadioModule } from '@angular/material/radio'; +import { ActionService } from 'src/app/services/action.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { atLeastOneFieldValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; +import { Message } from '@bufbuild/protobuf'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Condition } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; +import { startWith } from 'rxjs/operators'; + +export type ConditionType = NonNullable; +export type ConditionTypeValue = Omit< + NonNullable['value']>, + // we remove the message keys so $typeName is not required + keyof Message +>; + +@Component({ + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'cnsl-actions-two-add-action-condition', + templateUrl: './actions-two-add-action-condition.component.html', + styleUrls: ['./actions-two-add-action-condition.component.scss'], + imports: [ + TranslateModule, + MatRadioModule, + RouterModule, + ReactiveFormsModule, + InputModule, + MatAutocompleteModule, + MatCheckboxModule, + FormsModule, + CommonModule, + MatButtonModule, + MatProgressSpinnerModule, + ], +}) +export class ActionsTwoAddActionConditionComponent { + @Input({ required: true }) public set conditionType(conditionType: T) { + this.conditionType$.next(conditionType); + } + @Output() public readonly back = new EventEmitter(); + @Output() public readonly continue = new EventEmitter>(); + + private readonly conditionType$ = new ReplaySubject(1); + protected readonly form$: ReturnType; + + protected readonly executionServices$: Observable; + protected readonly executionMethods$: Observable; + protected readonly executionFunctions$: Observable; + + constructor( + private readonly fb: FormBuilder, + private readonly actionService: ActionService, + private readonly toast: ToastService, + private readonly destroyRef: DestroyRef, + ) { + this.form$ = this.buildForm().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.executionServices$ = this.listExecutionServices(this.form$).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.executionMethods$ = this.listExecutionMethods(this.form$).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.executionFunctions$ = this.listExecutionFunctions(this.form$).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + } + + public buildForm() { + return this.conditionType$.pipe( + switchMap((conditionType) => { + if (conditionType === 'event') { + return this.buildEventForm(); + } + if (conditionType === 'function') { + return this.buildFunctionForm(); + } + return this.buildRequestOrResponseForm(conditionType); + }), + ); + } + + private buildRequestOrResponseForm(requestOrResponse: T) { + const formFactory = () => ({ + case: requestOrResponse, + form: this.fb.group( + { + all: new FormControl(false, { nonNullable: true }), + service: new FormControl('', { nonNullable: true }), + method: new FormControl('', { nonNullable: true }), + }, + { + validators: atLeastOneFieldValidator(['all', 'service', 'method']), + }, + ), + }); + + return new Observable>((obs) => { + const form = formFactory(); + obs.next(form); + + const { all, service, method } = form.form.controls; + return all.valueChanges + .pipe( + map(() => all.value), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((all) => { + this.toggleFormControl(service, !all); + this.toggleFormControl(method, !all); + }); + }); + } + + public buildFunctionForm() { + return of({ + case: 'function' as const, + form: this.fb.group({ + name: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + }), + }); + } + + public buildEventForm() { + const formFactory = () => ({ + case: 'event' as const, + form: this.fb.group({ + all: new FormControl(false, { nonNullable: true }), + group: new FormControl('', { nonNullable: true }), + event: new FormControl('', { nonNullable: true }), + }), + }); + + return new Observable>((obs) => { + const form = formFactory(); + obs.next(form); + + const { all, group, event } = form.form.controls; + return all.valueChanges + .pipe( + map(() => all.value), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((all) => { + this.toggleFormControl(group, !all); + this.toggleFormControl(event, !all); + }); + }); + } + + private toggleFormControl(control: FormControl, enabled: boolean) { + if (enabled) { + control.enable(); + } else { + control.disable(); + } + } + + private listExecutionServices(form$: typeof this.form$) { + return defer(() => this.actionService.listExecutionServices({})).pipe( + map(({ services }) => services), + this.formFilter(form$, (form) => { + if ('service' in form.form.controls) { + return form.form.controls.service; + } + return undefined; + }), + catchError((error) => { + this.toast.showError(error); + return of([]); + }), + ); + } + + private listExecutionFunctions(form$: typeof this.form$) { + return defer(() => this.actionService.listExecutionFunctions({})).pipe( + map(({ functions }) => functions), + this.formFilter(form$, (form) => { + if (form.case !== 'function') { + return undefined; + } + return form.form.controls.name; + }), + catchError((error) => { + this.toast.showError(error); + return of([]); + }), + ); + } + + private listExecutionMethods(form$: typeof this.form$) { + return defer(() => this.actionService.listExecutionMethods({})).pipe( + map(({ methods }) => methods), + this.formFilter(form$, (form) => { + if ('method' in form.form.controls) { + return form.form.controls.method; + } + return undefined; + }), + // we also filter by service name + this.formFilter(form$, (form) => { + if ('service' in form.form.controls) { + return form.form.controls.service; + } + return undefined; + }), + catchError((error) => { + this.toast.showError(error); + return of([]); + }), + ); + } + + private formFilter( + form$: typeof this.form$, + getter: (form: ObservedValueOf) => FormControl | undefined, + ): OperatorFunction { + const filterValue$ = form$.pipe( + map(getter), + switchMap((control) => { + if (!control) { + return of(''); + } + + return control.valueChanges.pipe( + startWith(control.value), + map((value) => value.toLowerCase()), + ); + }), + ); + + return (obs) => + obs.pipe( + combineLatestWith(filterValue$), + map(([values, filterValue]) => values.filter((v) => v.toLowerCase().includes(filterValue))), + ); + } + + protected submit(form: ObservedValueOf) { + if (form.case === 'request' || form.case === 'response') { + (this as unknown as ActionsTwoAddActionConditionComponent<'request' | 'response'>).submitRequestOrResponse(form); + } else if (form.case === 'event') { + (this as unknown as ActionsTwoAddActionConditionComponent<'event'>).submitEvent(form); + } else if (form.case === 'function') { + (this as unknown as ActionsTwoAddActionConditionComponent<'function'>).submitFunction(form); + } + } + + private submitRequestOrResponse( + this: ActionsTwoAddActionConditionComponent<'request' | 'response'>, + { form }: ObservedValueOf>, + ) { + const { all, service, method } = form.getRawValue(); + + if (all) { + this.continue.emit({ + condition: { + case: 'all', + value: true, + }, + }); + } else if (method) { + this.continue.emit({ + condition: { + case: 'method', + value: method, + }, + }); + } else if (service) { + this.continue.emit({ + condition: { + case: 'service', + value: service, + }, + }); + } + } + + private submitEvent( + this: ActionsTwoAddActionConditionComponent<'event'>, + { form }: ObservedValueOf>, + ) { + const { all, event, group } = form.getRawValue(); + if (all) { + this.continue.emit({ + condition: { + case: 'all', + value: true, + }, + }); + } else if (event) { + this.continue.emit({ + condition: { + case: 'event', + value: event, + }, + }); + } else if (group) { + this.continue.emit({ + condition: { + case: 'group', + value: group, + }, + }); + } + } + + private submitFunction( + this: ActionsTwoAddActionConditionComponent<'function'>, + { form }: ObservedValueOf>, + ) { + const { name } = form.getRawValue(); + this.continue.emit({ + name, + }); + } +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.html new file mode 100644 index 0000000000..cc6e989cf2 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.html @@ -0,0 +1,28 @@ +

{{ 'ACTIONSTWO.EXECUTION.DIALOG.CREATE_TITLE' | translate }}

+

{{ 'ACTIONSTWO.EXECUTION.DIALOG.UPDATE_TITLE' | translate }}

+ + +
+ + + + + +
+
diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.scss b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.scss new file mode 100644 index 0000000000..8223e63565 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.scss @@ -0,0 +1,19 @@ +.framework-change-block { + display: flex; + flex-direction: column; + align-items: stretch; +} + +.actions { + display: flex; + justify-content: space-between; + margin-top: 1rem; +} + +.hide { + visibility: hidden; +} + +.show { + visibility: visible; +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.ts new file mode 100644 index 0000000000..12ae6598cc --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-dialog.component.ts @@ -0,0 +1,141 @@ +import { Component, computed, effect, Inject, signal } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { TranslateModule } from '@ngx-translate/core'; +import { ActionsTwoAddActionTypeComponent } from './actions-two-add-action-type/actions-two-add-action-type.component'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { + ActionsTwoAddActionConditionComponent, + ConditionType, +} from './actions-two-add-action-condition/actions-two-add-action-condition.component'; +import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target/actions-two-add-action-target.component'; +import { CommonModule } from '@angular/common'; +import { + Condition, + Execution, + ExecutionTargetType, + ExecutionTargetTypeSchema, +} from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; +import { Subject } from 'rxjs'; +import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; + +enum Page { + Type, + Condition, + Target, +} + +export type CorrectlyTypedCondition = Condition & { conditionType: Extract }; + +type CorrectlyTypedTargets = { type: Extract }; + +export type CorrectlyTypedExecution = Omit & { + condition: CorrectlyTypedCondition; + targets: CorrectlyTypedTargets[]; +}; + +export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => { + if (!execution.condition?.conditionType?.case) { + throw new Error('Condition is required'); + } + const conditionType = execution.condition.conditionType; + + const condition = { + ...execution.condition, + conditionType, + }; + + return { + ...execution, + condition, + targets: execution.targets + .map(({ type }) => ({ type })) + .filter((target): target is CorrectlyTypedTargets => target.type.case === 'target'), + }; +}; + +export type ActionTwoAddActionDialogData = { + execution?: CorrectlyTypedExecution; +}; + +export type ActionTwoAddActionDialogResult = MessageInitShape; + +@Component({ + selector: 'cnsl-actions-two-add-action-dialog', + templateUrl: './actions-two-add-action-dialog.component.html', + styleUrls: ['./actions-two-add-action-dialog.component.scss'], + standalone: true, + imports: [ + CommonModule, + MatButtonModule, + MatDialogModule, + TranslateModule, + ActionsTwoAddActionTypeComponent, + ActionsTwoAddActionConditionComponent, + ActionsTwoAddActionTargetComponent, + ], +}) +export class ActionTwoAddActionDialogComponent { + protected readonly Page = Page; + protected readonly page = signal(Page.Type); + + protected readonly typeSignal = signal('request'); + protected readonly conditionSignal = signal['condition']>(undefined); + protected readonly targetsSignal = signal[]>([]); + + protected readonly continueSubject = new Subject(); + + protected readonly request = computed>(() => { + return { + condition: this.conditionSignal(), + targets: this.targetsSignal(), + }; + }); + + protected readonly preselectedTargetIds: string[] = []; + + constructor( + protected readonly dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) protected readonly data: ActionTwoAddActionDialogData, + ) { + effect(() => { + const currentPage = this.page(); + if (currentPage === Page.Target) { + this.continueSubject.next(); // Trigger the Subject to request condition form when the page changes to "Target" + } + }); + + if (!data?.execution) { + return; + } + + this.targetsSignal.set(data.execution.targets); + this.typeSignal.set(data.execution.condition.conditionType.case); + this.conditionSignal.set(data.execution.condition); + this.preselectedTargetIds = data.execution.targets.map((target) => target.type.value); + + this.page.set(Page.Target); // Set the initial page based on the provided execution data + } + + public continue() { + const currentPage = this.page(); + if (currentPage === Page.Type) { + this.page.set(Page.Condition); + } else if (currentPage === Page.Condition) { + this.page.set(Page.Target); + } else { + this.dialogRef.close(this.request()); + } + } + + public back() { + const currentPage = this.page(); + if (currentPage === Page.Target) { + this.page.set(Page.Condition); + } else if (currentPage === Page.Condition) { + this.page.set(Page.Type); + } else { + this.dialogRef.close(); + } + } +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html new file mode 100644 index 0000000000..422ed7991e --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html @@ -0,0 +1,72 @@ +
+

{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}

+ + + {{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.TARGET.DESCRIPTION' | translate }} + + + + + + + {{ target.name }} + + + + + + + + + + + + + + + + + + + +
Reorder + + Name + {{ row.name }} + + + + +
+ +
+ + + +
+
diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.scss b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.scss new file mode 100644 index 0000000000..deff15c680 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.scss @@ -0,0 +1,36 @@ +.target-description { + margin-bottom: 0; +} + +.actions { + display: flex; + justify-content: space-between; + + .fill-space { + font: 1; + } +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.cdk-drop-list-dragging .mat-row:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.drag-row { + backdrop-filter: blur(10px); +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.spec.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.spec.ts new file mode 100644 index 0000000000..b4c86a8481 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.spec.ts @@ -0,0 +1,20 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target.component'; + +describe('ActionsTwoAddActionTargetComponent', () => { + let component: ActionsTwoAddActionTargetComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ActionsTwoAddActionTargetComponent], + }); + fixture = TestBed.createComponent(ActionsTwoAddActionTargetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts new file mode 100644 index 0000000000..e04368f8f4 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts @@ -0,0 +1,226 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + EventEmitter, + Input, + Output, + signal, + Signal, +} from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatButtonModule } from '@angular/material/button'; +import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ReplaySubject, switchMap } from 'rxjs'; +import { MatRadioModule } from '@angular/material/radio'; +import { ActionService } from 'src/app/services/action.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { InputModule } from 'src/app/modules/input/input.module'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; +import { ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; +import { MatSelectModule } from '@angular/material/select'; +import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { startWith } from 'rxjs/operators'; +import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module'; +import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { minArrayLengthValidator } from '../../../form-field/validators/validators'; +import { ProjectRoleChipModule } from '../../../project-role-chip/project-role-chip.module'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TableActionsModule } from '../../../table-actions/table-actions.module'; + +@Component({ + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'cnsl-actions-two-add-action-target', + templateUrl: './actions-two-add-action-target.component.html', + styleUrls: ['./actions-two-add-action-target.component.scss'], + imports: [ + TranslateModule, + MatRadioModule, + RouterModule, + ReactiveFormsModule, + InputModule, + MatAutocompleteModule, + FormsModule, + ActionConditionPipeModule, + CommonModule, + MatButtonModule, + MatProgressSpinnerModule, + MatSelectModule, + MatTableModule, + TypeSafeCellDefModule, + CdkDrag, + CdkDropList, + ProjectRoleChipModule, + MatTooltipModule, + TableActionsModule, + ], +}) +export class ActionsTwoAddActionTargetComponent { + @Input() public hideBackButton = false; + @Input() + public set preselectedTargetIds(preselectedTargetIds: string[]) { + this.preselectedTargetIds$.next(preselectedTargetIds); + } + + @Output() public readonly back = new EventEmitter(); + @Output() public readonly continue = new EventEmitter[]>(); + + private readonly preselectedTargetIds$ = new ReplaySubject(1); + + protected readonly form: ReturnType; + protected readonly targets: ReturnType; + private readonly selectedTargetIds: Signal; + protected readonly selectableTargets: Signal; + protected readonly dataSource: MatTableDataSource; + + constructor( + private readonly fb: FormBuilder, + private readonly actionService: ActionService, + private readonly toast: ToastService, + ) { + this.form = this.buildForm(); + this.targets = this.listTargets(); + + this.selectedTargetIds = this.getSelectedTargetIds(this.form); + this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds); + this.dataSource = this.getDataSource(this.targets, this.selectedTargetIds); + } + + private buildForm() { + const preselectedTargetIds = toSignal(this.preselectedTargetIds$, { initialValue: [] as string[] }); + + return computed(() => { + return this.fb.group({ + autocomplete: new FormControl('', { nonNullable: true }), + selectedTargetIds: new FormControl(preselectedTargetIds(), { + nonNullable: true, + validators: [minArrayLengthValidator(1)], + }), + }); + }); + } + + private listTargets() { + const targetsSignal = signal({ state: 'loading' as 'loading' | 'loaded', targets: new Map() }); + + this.actionService + .listTargets({}) + .then(({ result }) => { + const targets = result.reduce((acc, target) => { + acc.set(target.id, target); + return acc; + }, new Map()); + + targetsSignal.set({ state: 'loaded', targets }); + }) + .catch((error) => { + this.toast.showError(error); + }); + + return computed(targetsSignal); + } + + private getSelectedTargetIds(form: typeof this.form) { + const selectedTargetIds$ = toObservable(form).pipe( + startWith(form()), + switchMap((form) => { + const { selectedTargetIds } = form.controls; + return selectedTargetIds.valueChanges.pipe(startWith(selectedTargetIds.value)); + }), + ); + return toSignal(selectedTargetIds$, { requireSync: true }); + } + + private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal) { + return computed(() => { + const targetsCopy = new Map(targets().targets); + for (const selectedTargetId of selectedTargetIds()) { + targetsCopy.delete(selectedTargetId); + } + return Array.from(targetsCopy.values()); + }); + } + + private getDataSource(targetsSignal: typeof this.targets, selectedTargetIdsSignal: Signal) { + const selectedTargets = computed(() => { + // get this out of the loop so angular can track this dependency + // even if targets is empty + const { targets, state } = targetsSignal(); + const selectedTargetIds = selectedTargetIdsSignal(); + + if (state === 'loading') { + return []; + } + + return selectedTargetIds.map((id) => { + const target = targets.get(id); + if (!target) { + throw new Error(`Target with id ${id} not found`); + } + return target; + }); + }); + + const dataSource = new MatTableDataSource(selectedTargets()); + effect(() => { + dataSource.data = selectedTargets(); + }); + + return dataSource; + } + + protected addTarget(target: Target) { + const { selectedTargetIds } = this.form().controls; + selectedTargetIds.setValue([target.id, ...selectedTargetIds.value]); + this.form().controls.autocomplete.setValue(''); + } + + protected removeTarget(index: number) { + const { selectedTargetIds } = this.form().controls; + const data = [...selectedTargetIds.value]; + data.splice(index, 1); + selectedTargetIds.setValue(data); + } + + protected drop(event: CdkDragDrop) { + const { selectedTargetIds } = this.form().controls; + + const data = [...selectedTargetIds.value]; + moveItemInArray(data, event.previousIndex, event.currentIndex); + selectedTargetIds.setValue(data); + } + + protected handleEnter(event: Event) { + const selectableTargets = this.selectableTargets(); + if (selectableTargets.length !== 1) { + return; + } + + event.preventDefault(); + this.addTarget(selectableTargets[0]); + } + + protected submit() { + const selectedTargets = this.selectedTargetIds().map((value) => ({ + type: { + case: 'target' as const, + value, + }, + })); + + this.continue.emit(selectedTargets); + } + + protected trackTarget(_: number, target: Target) { + return target.id; + } +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.html new file mode 100644 index 0000000000..d1d1e8a0dd --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.html @@ -0,0 +1,49 @@ +

{{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.DESCRIPTION' | translate }}

+ +
+
+ + +
+ {{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.REQUEST.TITLE' | translate }} + {{ + 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.REQUEST.DESCRIPTION' | translate + }} +
+
+
+ {{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.RESPONSE.TITLE' | translate }} + {{ + 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.RESPONSE.DESCRIPTION' | translate + }} +
+
+ {{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.EVENTS.TITLE' | translate }} + {{ + 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.EVENTS.DESCRIPTION' | translate + }} +
+
+ {{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.FUNCTIONS.TITLE' | translate }} + {{ + 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.FUNCTIONS.DESCRIPTION' | translate + }} +
+
+
+ +
+ + +
+
diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.scss b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.scss new file mode 100644 index 0000000000..91a96b2cd2 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.scss @@ -0,0 +1,20 @@ +.execution-radio-group { + .execution-radio-button { + display: block; + margin-bottom: 1rem; + + .execution-type-text { + display: flex; + flex-direction: column; + + .description { + font-size: 0.9rem; + } + } + } +} + +.actions { + display: flex; + justify-content: space-between; +} diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.spec.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.spec.ts new file mode 100644 index 0000000000..cd4b02dc47 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.spec.ts @@ -0,0 +1,20 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActionsTwoAddActionTypeComponent } from './actions-two-add-action-type.component'; + +describe('ActionsTwoAddActionTypeComponent', () => { + let component: ActionsTwoAddActionTypeComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ActionsTwoAddActionTypeComponent], + }); + fixture = TestBed.createComponent(ActionsTwoAddActionTypeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.ts new file mode 100644 index 0000000000..143016ef5f --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-type/actions-two-add-action-type.component.ts @@ -0,0 +1,53 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, signal } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatButtonModule } from '@angular/material/button'; +import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Observable, Subject, map, of, startWith, switchMap, tap } from 'rxjs'; +import { MatRadioModule } from '@angular/material/radio'; +import { ConditionType } from '../actions-two-add-action-condition/actions-two-add-action-condition.component'; + +// export enum ExecutionType { +// REQUEST = 'request', +// RESPONSE = 'response', +// EVENTS = 'event', +// FUNCTIONS = 'function', +// } + +@Component({ + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'cnsl-actions-two-add-action-type', + templateUrl: './actions-two-add-action-type.component.html', + styleUrls: ['./actions-two-add-action-type.component.scss'], + imports: [TranslateModule, MatRadioModule, RouterModule, ReactiveFormsModule, FormsModule, CommonModule, MatButtonModule], +}) +export class ActionsTwoAddActionTypeComponent { + protected readonly typeForm: ReturnType = this.buildActionTypeForm(); + @Output() public readonly typeChanges$: Observable; + + @Output() public readonly back = new EventEmitter(); + @Output() public readonly continue = new EventEmitter(); + @Input() public set initialValue(type: ConditionType) { + this.typeForm.get('executionType')!.setValue(type); + } + + constructor(private readonly fb: FormBuilder) { + this.typeChanges$ = this.typeForm.get('executionType')!.valueChanges.pipe( + startWith(this.typeForm.get('executionType')!.value), // Emit the initial value + ); + } + + public buildActionTypeForm() { + return this.fb.group({ + executionType: new FormControl('request', { + nonNullable: true, + }), + }); + } + + public submit() { + this.continue.emit(this.typeForm.get('executionType')!.value); + } +} diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html new file mode 100644 index 0000000000..37d4f89dd0 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html @@ -0,0 +1,65 @@ +

{{ 'ACTIONSTWO.TARGET.CREATE.TITLE' | translate }}

+ +

{{ 'ACTIONSTWO.TARGET.CREATE.DESCRIPTION' | translate }}

+ +
+ + {{ 'ACTIONSTWO.TARGET.CREATE.NAME' | translate }} + + {{ + 'ACTIONSTWO.TARGET.CREATE.NAME_DESCRIPTION' | translate + }} + + + + {{ 'ACTIONSTWO.TARGET.CREATE.ENDPOINT' | translate }} + + {{ + 'ACTIONSTWO.TARGET.CREATE.ENDPOINT_DESCRIPTION' | translate + }} + + + + {{ 'ACTIONSTWO.TARGET.CREATE.TYPE' | translate }} + + + {{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }} + + + + + + {{ 'ACTIONSTWO.TARGET.CREATE.TIMEOUT' | translate }} + + {{ + 'ACTIONSTWO.TARGET.CREATE.TIMEOUT_DESCRIPTION' | translate + }} + + + +
+ {{ 'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR' | translate }} + {{ + 'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR_DESCRIPTION' | translate + }} + {{ 'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR_WARNING' | translate }} + +
+
+
+
+
+ + + + +
diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss new file mode 100644 index 0000000000..34a7d5203d --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss @@ -0,0 +1,25 @@ +.target-checkbox { + margin-bottom: 1rem; + + .target-condition-text { + display: flex; + flex-direction: column; + + .description { + font-size: 13px; + } + } +} + +.target-description { + margin-bottom: 0; +} + +.actions { + display: flex; + justify-content: space-between; +} + +.name-hint { + font-size: 12px; +} diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts new file mode 100644 index 0000000000..7d3ad0e86c --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts @@ -0,0 +1,115 @@ +import { Component, Inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { InputModule } from '../../input/input.module'; +import { requiredValidator } from '../../form-field/validators/validators'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { DurationSchema } from '@bufbuild/protobuf/wkt'; +import { MatSelectModule } from '@angular/material/select'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; +import { + CreateTargetRequestSchema, + UpdateTargetRequestSchema, +} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; + +type TargetTypes = ActionTwoAddTargetDialogComponent['targetTypes'][number]; + +@Component({ + selector: 'cnsl-actions-two-add-target-dialog', + templateUrl: './actions-two-add-target-dialog.component.html', + styleUrls: ['./actions-two-add-target-dialog.component.scss'], + standalone: true, + imports: [ + CommonModule, + MatButtonModule, + MatDialogModule, + ReactiveFormsModule, + TranslateModule, + InputModule, + MatCheckboxModule, + MatSelectModule, + ], +}) +export class ActionTwoAddTargetDialogComponent { + protected readonly targetTypes = ['restCall', 'restWebhook', 'restAsync'] as const; + protected readonly targetForm: ReturnType; + + constructor( + private fb: FormBuilder, + public dialogRef: MatDialogRef< + ActionTwoAddTargetDialogComponent, + MessageInitShape + >, + @Inject(MAT_DIALOG_DATA) public readonly data: { target?: Target }, + ) { + this.targetForm = this.buildTargetForm(); + + if (!data?.target) { + return; + } + + this.targetForm.patchValue({ + name: data.target.name, + endpoint: data.target.endpoint, + timeout: Number(data.target.timeout?.seconds), + type: this.data.target?.targetType?.case ?? 'restWebhook', + interruptOnError: + data.target.targetType.case === 'restWebhook' || data.target.targetType.case === 'restCall' + ? data.target.targetType.value.interruptOnError + : false, + }); + } + + public buildTargetForm() { + return this.fb.group({ + name: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + type: new FormControl('restWebhook', { + nonNullable: true, + validators: [requiredValidator], + }), + endpoint: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + timeout: new FormControl(10, { nonNullable: true, validators: [requiredValidator] }), + interruptOnError: new FormControl(false, { nonNullable: true }), + }); + } + + public closeWithResult() { + if (this.targetForm.invalid) { + return; + } + + const { type, name, endpoint, timeout, interruptOnError } = this.targetForm.getRawValue(); + + const timeoutDuration: MessageInitShape = { + seconds: BigInt(timeout), + nanos: 0, + }; + + const targetType: Extract['targetType'], { case: TargetTypes }> = + type === 'restWebhook' + ? { case: type, value: { interruptOnError } } + : type === 'restCall' + ? { case: type, value: { interruptOnError } } + : { case: 'restAsync', value: {} }; + + const baseReq = { + name, + endpoint, + timeout: timeoutDuration, + targetType, + }; + + this.dialogRef.close( + this.data.target + ? { + ...baseReq, + id: this.data.target.id, + } + : baseReq, + ); + } +} diff --git a/console/src/app/modules/actions-two/actions-two-routing.module.ts b/console/src/app/modules/actions-two/actions-two-routing.module.ts new file mode 100644 index 0000000000..7ce79d01ea --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ActionsTwoActionsComponent } from './actions-two-actions/actions-two-actions.component'; + +const routes: Routes = [ + { + path: '', + component: ActionsTwoActionsComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class ActionsTwoRoutingModule {} diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.html b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.html new file mode 100644 index 0000000000..1cac09f1e4 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.html @@ -0,0 +1,82 @@ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'ACTIONSTWO.TARGET.TABLE.ID' | translate }} + {{ row.id }} + {{ 'ACTIONSTWO.TARGET.TABLE.NAME' | translate }} +
+ {{ row.name }} +
+
{{ 'ACTIONSTWO.TARGET.TABLE.ENDPOINT' | translate }} + {{ row.endpoint }} + {{ 'ACTIONSTWO.TARGET.TABLE.CREATIONDATE' | translate }} + {{ row.creationDate | timestampToDate | localizedDate: 'regular' }} + + + + +
+
+
diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.scss b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.scss new file mode 100644 index 0000000000..ff20def7da --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.scss @@ -0,0 +1,4 @@ +.target-key { + display: flex; + white-space: nowrap; +} diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.ts b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.ts new file mode 100644 index 0000000000..2b2e4db579 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets-table/actions-two-targets-table.component.ts @@ -0,0 +1,49 @@ +import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; +import { filter, startWith } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; +import { toSignal } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'cnsl-actions-two-targets-table', + templateUrl: './actions-two-targets-table.component.html', + styleUrls: ['./actions-two-targets-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ActionsTwoTargetsTableComponent { + @Output() + public readonly refresh = new EventEmitter(); + + @Output() + public readonly selected = new EventEmitter(); + + @Output() + public readonly delete = new EventEmitter(); + + @Input({ required: true }) + public set targets(targets: Target[] | null) { + this.targets$.next(targets); + } + + protected readonly targets$ = new ReplaySubject(1); + protected readonly dataSource: MatTableDataSource; + + constructor() { + this.dataSource = this.getDataSource(); + } + + private getDataSource() { + const targets$ = this.targets$.pipe(filter(Boolean), startWith([])); + const targetsSignal = toSignal(targets$, { requireSync: true }); + + const dataSource = new MatTableDataSource(targetsSignal()); + effect(() => { + const targets = targetsSignal(); + if (dataSource.data !== targets) { + dataSource.data = targets; + } + }); + return dataSource; + } +} diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html new file mode 100644 index 0000000000..a6bde66e41 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html @@ -0,0 +1,13 @@ +

{{ 'ACTIONSTWO.TARGET.TITLE' | translate }}

+

{{ 'ACTIONSTWO.TARGET.DESCRIPTION' | translate }}

+ + + + diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.scss b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.ts b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.ts new file mode 100644 index 0000000000..45bee14605 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.ts @@ -0,0 +1,89 @@ +import { ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core'; +import { lastValueFrom, Observable, of, ReplaySubject } from 'rxjs'; +import { ActionService } from 'src/app/services/action.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { catchError, map, startWith, switchMap } from 'rxjs/operators'; +import { MatDialog } from '@angular/material/dialog'; +import { ActionTwoAddTargetDialogComponent } from '../actions-two-add-target/actions-two-add-target-dialog.component'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb'; +import { + CreateTargetRequestSchema, + UpdateTargetRequestSchema, +} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; + +@Component({ + selector: 'cnsl-actions-two-targets', + templateUrl: './actions-two-targets.component.html', + styleUrls: ['./actions-two-targets.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ActionsTwoTargetsComponent { + protected readonly targets$: Observable; + protected readonly refresh$ = new ReplaySubject(1); + + constructor( + private readonly actionService: ActionService, + private readonly toast: ToastService, + private readonly destroyRef: DestroyRef, + private readonly dialog: MatDialog, + ) { + this.targets$ = this.getTargets$(); + } + + private getTargets$() { + return this.refresh$.pipe( + startWith(true), + switchMap(() => { + return this.actionService.listTargets({}); + }), + map(({ result }) => result), + catchError((err) => { + this.toast.showError(err); + return of([]); + }), + ); + } + + public async deleteTarget(target: Target) { + await this.actionService.deleteTarget({ id: target.id }); + await new Promise((res) => setTimeout(res, 1000)); + this.refresh$.next(true); + } + + public async openDialog(target?: Target) { + const request$ = this.dialog + .open< + ActionTwoAddTargetDialogComponent, + { target?: Target }, + MessageInitShape + >(ActionTwoAddTargetDialogComponent, { + width: '550px', + data: { + target: target, + }, + }) + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)); + + const request = await lastValueFrom(request$); + if (!request) { + return; + } + + try { + if ('id' in request) { + await this.actionService.updateTarget(request); + } else { + await this.actionService.createTarget(request); + } + + await new Promise((res) => setTimeout(res, 1000)); + this.refresh$.next(true); + } catch (error) { + console.error(error); + this.toast.showError(error); + } + } +} diff --git a/console/src/app/modules/actions-two/actions-two.module.ts b/console/src/app/modules/actions-two/actions-two.module.ts new file mode 100644 index 0000000000..45d70193f9 --- /dev/null +++ b/console/src/app/modules/actions-two/actions-two.module.ts @@ -0,0 +1,53 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActionsTwoActionsComponent } from './actions-two-actions/actions-two-actions.component'; +import { ActionsTwoTargetsComponent } from './actions-two-targets/actions-two-targets.component'; +import { ActionsTwoRoutingModule } from './actions-two-routing.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatButtonModule } from '@angular/material/button'; +import { ActionsTwoTargetsTableComponent } from './actions-two-targets/actions-two-targets-table/actions-two-targets-table.component'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TableActionsModule } from '../table-actions/table-actions.module'; +import { RefreshTableModule } from '../refresh-table/refresh-table.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ActionKeysModule } from '../action-keys/action-keys.module'; +import { ActionsTwoActionsTableComponent } from './actions-two-actions/actions-two-actions-table/actions-two-actions-table.component'; +import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; +import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; +import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module'; +import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.module'; +import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module'; +import { MatSelectModule } from '@angular/material/select'; +import { MatIconModule } from '@angular/material/icon'; + +@NgModule({ + declarations: [ + ActionsTwoActionsComponent, + ActionsTwoTargetsComponent, + ActionsTwoTargetsTableComponent, + ActionsTwoActionsTableComponent, + ], + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + TableActionsModule, + TimestampToDatePipeModule, + ActionsTwoRoutingModule, + LocalizedDatePipeModule, + ReactiveFormsModule, + TranslateModule, + MatTableModule, + MatTooltipModule, + MatSelectModule, + RefreshTableModule, + ActionKeysModule, + MatIconModule, + TypeSafeCellDefModule, + ProjectRoleChipModule, + ActionConditionPipeModule, + ], + exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent], +}) +export default class ActionsTwoModule {} diff --git a/console/src/app/modules/client-keys/client-keys.component.html b/console/src/app/modules/client-keys/client-keys.component.html index 8ac5869c8d..dfb99a5e17 100644 --- a/console/src/app/modules/client-keys/client-keys.component.html +++ b/console/src/app/modules/client-keys/client-keys.component.html @@ -1,7 +1,6 @@ diff --git a/console/src/app/modules/events/events.component.html b/console/src/app/modules/events/events.component.html index 556a602441..7dad189da2 100644 --- a/console/src/app/modules/events/events.component.html +++ b/console/src/app/modules/events/events.component.html @@ -6,12 +6,7 @@

{{ 'DESCRIPTIONS.SETTINGS.IAM_EVENTS.DESCRIPTION' | translate }}

- +
diff --git a/console/src/app/modules/failed-events/failed-events.component.html b/console/src/app/modules/failed-events/failed-events.component.html index 0073ca4075..4b7c792f1f 100644 --- a/console/src/app/modules/failed-events/failed-events.component.html +++ b/console/src/app/modules/failed-events/failed-events.component.html @@ -2,7 +2,7 @@

{{ 'DESCRIPTIONS.SETTINGS.IAM_FAILED_EVENTS.DESCRIPTION' | translate }}

- + diff --git a/console/src/app/modules/filter-org/filter-org.component.ts b/console/src/app/modules/filter-org/filter-org.component.ts index 8e100971d0..220b219358 100644 --- a/console/src/app/modules/filter-org/filter-org.component.ts +++ b/console/src/app/modules/filter-org/filter-org.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, DestroyRef, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { ActivatedRoute, Router } from '@angular/router'; import { take } from 'rxjs'; @@ -27,9 +27,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit { constructor( router: Router, + destroyRef: DestroyRef, protected override route: ActivatedRoute, ) { - super(router, route); + super(router, route, destroyRef); } ngOnInit(): void { diff --git a/console/src/app/modules/filter-project/filter-project.component.ts b/console/src/app/modules/filter-project/filter-project.component.ts index b884024c2c..92556d311d 100644 --- a/console/src/app/modules/filter-project/filter-project.component.ts +++ b/console/src/app/modules/filter-project/filter-project.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, DestroyRef, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { ActivatedRoute, Router } from '@angular/router'; import { take } from 'rxjs'; @@ -23,8 +23,8 @@ export class FilterProjectComponent extends FilterComponent implements OnInit { public searchQueries: ProjectQuery[] = []; public states: ProjectState[] = [ProjectState.PROJECT_STATE_ACTIVE, ProjectState.PROJECT_STATE_INACTIVE]; - constructor(router: Router, route: ActivatedRoute) { - super(router, route); + constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) { + super(router, route, destroyRef); } ngOnInit(): void { diff --git a/console/src/app/modules/filter-user-grants/filter-user-grants.component.ts b/console/src/app/modules/filter-user-grants/filter-user-grants.component.ts index 3c17e8c208..dccaed13e5 100644 --- a/console/src/app/modules/filter-user-grants/filter-user-grants.component.ts +++ b/console/src/app/modules/filter-user-grants/filter-user-grants.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, DestroyRef, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { ActivatedRoute, Router } from '@angular/router'; import { take } from 'rxjs'; @@ -29,8 +29,8 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit public SubQuery: any = SubQuery; public searchQueries: UserGrantQuery[] = []; - constructor(router: Router, route: ActivatedRoute) { - super(router, route); + constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) { + super(router, route, destroyRef); } ngOnInit(): void { diff --git a/console/src/app/modules/filter-user/filter-user.component.html b/console/src/app/modules/filter-user/filter-user.component.html index c5d3d9a820..907ea6d18d 100644 --- a/console/src/app/modules/filter-user/filter-user.component.html +++ b/console/src/app/modules/filter-user/filter-user.component.html @@ -1,4 +1,4 @@ - +
{ - const { filter } = params; - if (filter) { - const stringifiedFilters = filter as string; + this.route.queryParamMap + .pipe( + take(1), + map((params) => params.get('filter')), + filter(Boolean), + ) + .subscribe((stringifiedFilters) => { const filters: UserSearchQuery.AsObject[] = JSON.parse(stringifiedFilters) as UserSearchQuery.AsObject[]; const userQueries = filters.map((filter) => { @@ -94,8 +97,7 @@ export class FilterUserComponent extends FilterComponent implements OnInit { this.filterChanged.emit(this.searchQueries ? this.searchQueries : []); // this.showFilter = true; // this.filterOpen.emit(true); - } - }); + }); } public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) { diff --git a/console/src/app/modules/filter/filter.component.ts b/console/src/app/modules/filter/filter.component.ts index ce2cc15c08..dac94525d9 100644 --- a/console/src/app/modules/filter/filter.component.ts +++ b/console/src/app/modules/filter/filter.component.ts @@ -1,7 +1,6 @@ import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay'; -import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { Component, DestroyRef, EventEmitter, Input, Output } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { Observable, Subject, takeUntil } from 'rxjs'; import { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb'; import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb'; import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb'; @@ -9,6 +8,7 @@ import { ProjectQuery } from 'src/app/proto/generated/zitadel/project_pb'; import { SearchQuery as UserSearchQuery, UserGrantQuery } from 'src/app/proto/generated/zitadel/user_pb'; import { ActionKeysType } from '../action-keys/action-keys.component'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; type FilterSearchQuery = UserSearchQuery | MemberSearchQuery | UserGrantQuery | ProjectQuery | OrgQuery; type FilterSearchQueryAsObject = @@ -23,7 +23,7 @@ type FilterSearchQueryAsObject = templateUrl: './filter.component.html', styleUrls: ['./filter.component.scss'], }) -export class FilterComponent implements OnDestroy { +export class FilterComponent { @Output() public filterChanged: EventEmitter = new EventEmitter(); @Output() public filterOpen: EventEmitter = new EventEmitter(false); @@ -32,9 +32,6 @@ export class FilterComponent implements OnDestroy { @Input() public queryCount: number = 0; - private destroy$: Subject = new Subject(); - public filterChanged$: Observable = this.filterChanged.asObservable(); - public showFilter: boolean = false; public methods: TextQueryMethod[] = [ TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, @@ -59,17 +56,13 @@ export class FilterComponent implements OnDestroy { this.trigger.emit(); } - public ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - constructor( private router: Router, protected route: ActivatedRoute, + destroyRef: DestroyRef, ) { const changes$ = this.filterChanged.asObservable(); - changes$.pipe(takeUntil(this.destroy$)).subscribe((queries) => { + changes$.pipe(takeUntilDestroyed(destroyRef)).subscribe((queries) => { const filters: Array | undefined = queries ?.map((q) => q.toObject()) .map((query) => @@ -81,15 +74,17 @@ export class FilterComponent implements OnDestroy { ); if (filters && Object.keys(filters)) { - this.router.navigate([], { - relativeTo: this.route, - queryParams: { - ['filter']: JSON.stringify(filters), - }, - replaceUrl: true, - queryParamsHandling: 'merge', - skipLocationChange: false, - }); + this.router + .navigate([], { + relativeTo: this.route, + queryParams: { + ['filter']: JSON.stringify(filters), + }, + replaceUrl: true, + queryParamsHandling: 'merge', + skipLocationChange: false, + }) + .then(); } }); } diff --git a/console/src/app/modules/footer/footer.component.html b/console/src/app/modules/footer/footer.component.html index 26d863d129..b9eda2d7db 100644 --- a/console/src/app/modules/footer/footer.component.html +++ b/console/src/app/modules/footer/footer.component.html @@ -1,6 +1,6 @@
{{ 'IAM.FAILEDEVENTS.VIEWNAME' | translate }}
diff --git a/console/src/app/modules/idp-table/idp-table.component.html b/console/src/app/modules/idp-table/idp-table.component.html index 7188040897..d67659eafc 100644 --- a/console/src/app/modules/idp-table/idp-table.component.html +++ b/console/src/app/modules/idp-table/idp-table.component.html @@ -1,7 +1,6 @@ - {{ 'USER.DATA.STATE' + user.state | translate }} + {{ (isV2(user) ? 'USER.STATEV2.' : 'USER.STATE.') + user.state | translate }}

{{ 'USER.ID' | translate }}

-

{{ user.id }}

+

{{ userId }}

{{ 'USER.DETAILS.DATECREATED' | translate }}

-

- {{ user.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }} +

+ {{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}

{{ 'USER.DETAILS.DATECHANGED' | translate }}

-

- {{ user.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }} +

+ {{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}

diff --git a/console/src/app/modules/info-row/info-row.component.ts b/console/src/app/modules/info-row/info-row.component.ts index 35e682e4a1..85d6a1bdc6 100644 --- a/console/src/app/modules/info-row/info-row.component.ts +++ b/console/src/app/modules/info-row/info-row.component.ts @@ -6,6 +6,9 @@ import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb'; import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { GrantedProject, Project, ProjectGrantState, ProjectState } from 'src/app/proto/generated/zitadel/project_pb'; import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; +import { User as UserV1 } from '@zitadel/proto/zitadel/user_pb'; +import { User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { LoginPolicy as LoginPolicyV2 } from '@zitadel/proto/zitadel/policy_pb'; @Component({ selector: 'cnsl-info-row', @@ -13,14 +16,14 @@ import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; styleUrls: ['./info-row.component.scss'], }) export class InfoRowComponent { - @Input() public user!: User.AsObject; + @Input() public user?: User.AsObject | UserV2 | UserV1; @Input() public org!: Org.AsObject; @Input() public instance!: InstanceDetail.AsObject; @Input() public app!: App.AsObject; @Input() public idp!: IDP.AsObject; @Input() public project!: Project.AsObject; @Input() public grantedProject!: GrantedProject.AsObject; - @Input() public loginPolicy?: LoginPolicy.AsObject; + @Input() public loginPolicy?: LoginPolicy.AsObject | LoginPolicyV2; public UserState: any = UserState; public State: any = State; @@ -35,25 +38,74 @@ export class InfoRowComponent { constructor() {} public get loginMethods(): Set { - const methods = this.user?.loginNamesList; - let email: string = ''; - let phone: string = ''; - if (this.loginPolicy) { - if ( - !this.loginPolicy?.disableLoginWithEmail && - this.user.human?.email?.email && - this.user.human.email.isEmailVerified - ) { - email = this.user.human?.email?.email; - } - if ( - !this.loginPolicy?.disableLoginWithPhone && - this.user.human?.phone?.phone && - this.user.human.phone.isPhoneVerified - ) { - phone = this.user.human?.phone?.phone; - } + if (!this.user) { + return new Set(); } - return new Set([email, phone, ...methods].filter((method) => !!method)); + + const methods = '$typeName' in this.user ? this.user.loginNames : this.user.loginNamesList; + + const loginPolicy = this.loginPolicy; + if (!loginPolicy) { + return new Set([...methods]); + } + + let email = !loginPolicy.disableLoginWithEmail ? this.getEmail(this.user) : ''; + let phone = !loginPolicy.disableLoginWithPhone ? this.getPhone(this.user) : ''; + + return new Set([email, phone, ...methods].filter(Boolean)); + } + + public get userId() { + if (!this.user) { + return undefined; + } + if ('$typeName' in this.user && this.user.$typeName === 'zitadel.user.v2.User') { + return this.user.userId; + } + return this.user.id; + } + + public get changeDate() { + return this.user?.details?.changeDate; + } + + public get creationDate() { + return this.user?.details?.creationDate; + } + + private getEmail(user: User.AsObject | UserV2 | UserV1) { + const human = this.human(user); + if (!human) { + return ''; + } + if ('$typeName' in human && human.$typeName === 'zitadel.user.v2.HumanUser') { + return human.email?.isVerified ? human.email.email : ''; + } + return human.email?.isEmailVerified ? human.email.email : ''; + } + + private getPhone(user: User.AsObject | UserV2 | UserV1) { + const human = this.human(user); + if (!human) { + return ''; + } + if ('$typeName' in human && human.$typeName === 'zitadel.user.v2.HumanUser') { + return human.phone?.isVerified ? human.phone.phone : ''; + } + return human.phone?.isPhoneVerified ? human.phone.phone : ''; + } + + public human(user: User.AsObject | UserV2 | UserV1) { + if (!('$typeName' in user)) { + return user.human; + } + return user.type.case === 'human' ? user.type.value : undefined; + } + + public isV2(user: User.AsObject | UserV2 | UserV1) { + if ('$typeName' in user) { + return user.$typeName === 'zitadel.user.v2.User'; + } + return false; } } diff --git a/console/src/app/modules/input/input.directive.ts b/console/src/app/modules/input/input.directive.ts index a7e2f644fc..cc816aa18d 100644 --- a/console/src/app/modules/input/input.directive.ts +++ b/console/src/app/modules/input/input.directive.ts @@ -71,14 +71,7 @@ const _MatInputBase = mixinErrorState( }) export class InputDirective extends _MatInputBase - implements - MatFormFieldControl, - OnChanges, - CanUpdateErrorState, - OnDestroy, - AfterViewInit, - DoCheck, - CanUpdateErrorState + implements MatFormFieldControl, OnChanges, CanUpdateErrorState, OnDestroy, AfterViewInit, DoCheck, CanUpdateErrorState { protected _uid: string = `cnsl-input-${nextUniqueId++}`; protected _previousNativeValue: any; diff --git a/console/src/app/modules/machine-keys/machine-keys.component.html b/console/src/app/modules/machine-keys/machine-keys.component.html index e96cad5194..b11c43d4d5 100644 --- a/console/src/app/modules/machine-keys/machine-keys.component.html +++ b/console/src/app/modules/machine-keys/machine-keys.component.html @@ -1,7 +1,6 @@ diff --git a/console/src/app/modules/members-table/members-table.component.html b/console/src/app/modules/members-table/members-table.component.html index 3b2e70cff7..c3a0b659e2 100644 --- a/console/src/app/modules/members-table/members-table.component.html +++ b/console/src/app/modules/members-table/members-table.component.html @@ -1,7 +1,6 @@ Promise; + removeFcn: (key: string) => Promise; +}; @Component({ selector: 'cnsl-metadata-dialog', @@ -10,72 +18,63 @@ import { ToastService } from 'src/app/services/toast.service'; styleUrls: ['./metadata-dialog.component.scss'], }) export class MetadataDialogComponent { - public metadata: Partial[] = []; + public metadata: { key: string; value: string }[] = []; public ts!: Timestamp.AsObject | undefined; constructor( private toast: ToastService, public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any, + @Inject(MAT_DIALOG_DATA) public data: MetadataDialogData, ) { - this.metadata = data.metadata; + this.metadata = data.metadata.map(({ key, value }) => ({ + key, + value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'), + })); } public addEntry(): void { - const newGroup = { + this.metadata.push({ key: '', value: '', - }; - - this.metadata.push(newGroup); + }); } - public removeEntry(index: number): void { + public async removeEntry(index: number) { const key = this.metadata[index].key; - if (key) { - this.removeMetadata(key).then(() => { - this.metadata.splice(index, 1); - if (this.metadata.length === 0) { - this.addEntry(); - } - }); - } else { + if (!key) { this.metadata.splice(index, 1); + return; + } + + try { + await this.data.removeFcn(key); + } catch (error) { + this.toast.showError(error); + return; + } + + this.toast.showInfo('METADATA.REMOVESUCCESS', true); + this.metadata.splice(index, 1); + if (this.metadata.length === 0) { + this.addEntry(); } } - public saveElement(index: number): void { - const metadataElement = this.metadata[index]; + public async saveElement(index: number) { + const { key, value } = this.metadata[index]; - if (metadataElement.key && metadataElement.value) { - this.setMetadata(metadataElement.key, metadataElement.value as string); + if (!key || !value) { + return; } - } - public setMetadata(key: string, value: string): void { - if (key && value) { - this.data - .setFcn(key, value) - .then(() => { - this.toast.showInfo('METADATA.SETSUCCESS', true); - }) - .catch((error: any) => { - this.toast.showError(error); - }); + try { + await this.data.setFcn(key, value); + this.toast.showInfo('METADATA.SETSUCCESS', true); + } catch (error) { + this.toast.showError(error); } } - public removeMetadata(key: string): Promise { - return this.data - .removeFcn(key) - .then((resp: any) => { - this.toast.showInfo('METADATA.REMOVESUCCESS', true); - }) - .catch((error: any) => { - this.toast.showError(error); - }); - } - closeDialog(): void { this.dialogRef.close(); } diff --git a/console/src/app/modules/metadata/metadata/metadata.component.html b/console/src/app/modules/metadata/metadata/metadata.component.html index acd0b407f6..a4bbcd33df 100644 --- a/console/src/app/modules/metadata/metadata/metadata.component.html +++ b/console/src/app/modules/metadata/metadata/metadata.component.html @@ -1,7 +1,7 @@
{{ 'IAM.VIEWS.VIEWNAME' | translate }}
-
+
{{ 'USER.MFA.EMPTY' | translate }}
diff --git a/console/src/app/modules/metadata/metadata/metadata.component.ts b/console/src/app/modules/metadata/metadata/metadata.component.ts index bb784ac433..7f72297c00 100644 --- a/console/src/app/modules/metadata/metadata/metadata.component.ts +++ b/console/src/app/modules/metadata/metadata/metadata.component.ts @@ -1,16 +1,26 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { MatSort } from '@angular/material/sort'; -import { MatTable, MatTableDataSource } from '@angular/material/table'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { MatTableDataSource } from '@angular/material/table'; +import { Observable, ReplaySubject } from 'rxjs'; +import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; +import { map, startWith } from 'rxjs/operators'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; +import { Buffer } from 'buffer'; + +type StringMetadata = { + key: string; + value: string; +}; @Component({ selector: 'cnsl-metadata', templateUrl: './metadata.component.html', styleUrls: ['./metadata.component.scss'], }) -export class MetadataComponent implements OnChanges { - @Input() public metadata: Metadata.AsObject[] = []; +export class MetadataComponent implements OnInit { + @Input({ required: true }) public set metadata(metadata: (Metadata.AsObject | MetadataV2)[]) { + this.metadata$.next(metadata); + } @Input() public disabled: boolean = false; @Input() public loading: boolean = false; @Input({ required: true }) public description!: string; @@ -18,18 +28,23 @@ export class MetadataComponent implements OnChanges { @Output() public refresh: EventEmitter = new EventEmitter(); public displayedColumns: string[] = ['key', 'value']; - private loadingSubject: BehaviorSubject = new BehaviorSubject(false); - public loading$: Observable = this.loadingSubject.asObservable(); + public metadata$ = new ReplaySubject<(Metadata.AsObject | MetadataV2)[]>(1); + public dataSource$?: Observable>; - @ViewChild(MatTable) public table!: MatTable; @ViewChild(MatSort) public sort!: MatSort; - public dataSource: MatTableDataSource = new MatTableDataSource([]); constructor() {} - ngOnChanges(changes: SimpleChanges): void { - if (changes['metadata']?.currentValue) { - this.dataSource = new MatTableDataSource(changes['metadata'].currentValue); - } + ngOnInit() { + this.dataSource$ = this.metadata$.pipe( + map((metadata) => + metadata.map(({ key, value }) => ({ + key, + value: Buffer.from(value as any as string, 'base64').toString('utf-8'), + })), + ), + startWith([] as StringMetadata[]), + map((metadata) => new MatTableDataSource(metadata)), + ); } } diff --git a/console/src/app/modules/org-table/org-table.component.html b/console/src/app/modules/org-table/org-table.component.html index 042988054b..7322800ae3 100644 --- a/console/src/app/modules/org-table/org-table.component.html +++ b/console/src/app/modules/org-table/org-table.component.html @@ -1,9 +1,4 @@ - + diff --git a/console/src/app/modules/org-table/org-table.component.ts b/console/src/app/modules/org-table/org-table.component.ts index 24bd9ea2af..ea6cf16110 100644 --- a/console/src/app/modules/org-table/org-table.component.ts +++ b/console/src/app/modules/org-table/org-table.component.ts @@ -36,7 +36,6 @@ export class OrgTableComponent { private loadingSubject: BehaviorSubject = new BehaviorSubject(false); public loading$: Observable = this.loadingSubject.asObservable(); public activeOrg!: Org.AsObject; - public OrgListSearchKey: any = OrgListSearchKey; public initialLimit: number = 20; public timestamp: Timestamp.AsObject | undefined = undefined; public totalResult: number = 0; diff --git a/console/src/app/modules/paginator/paginator.component.ts b/console/src/app/modules/paginator/paginator.component.ts index 4fd62fb568..74f487dcb5 100644 --- a/console/src/app/modules/paginator/paginator.component.ts +++ b/console/src/app/modules/paginator/paginator.component.ts @@ -1,5 +1,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb'; +import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt'; export interface PageEvent { length: number; @@ -14,7 +15,7 @@ export interface PageEvent { styleUrls: ['./paginator.component.scss'], }) export class PaginatorComponent { - @Input() public timestamp: Timestamp.AsObject | undefined = undefined; + @Input() public timestamp: Timestamp.AsObject | ConnectTimestamp | undefined = undefined; @Input() public length: number = 0; @Input() public pageSize: number = 10; @Input() public pageIndex: number = 0; diff --git a/console/src/app/modules/password-complexity-view/password-complexity-view.component.html b/console/src/app/modules/password-complexity-view/password-complexity-view.component.html index 0a5649dabb..169ce8ef1d 100644 --- a/console/src/app/modules/password-complexity-view/password-complexity-view.component.html +++ b/console/src/app/modules/password-complexity-view/password-complexity-view.component.html @@ -1,4 +1,4 @@ -
+
@@ -15,7 +15,7 @@ diameter="20" [color]="currentError ? 'warn' : 'valid'" mode="determinate" - [value]="(password?.value?.length / policy.minLength) * 100" + [value]="(password?.value?.length / minLength) * 100" >
diff --git a/console/src/app/modules/password-complexity-view/password-complexity-view.component.ts b/console/src/app/modules/password-complexity-view/password-complexity-view.component.ts index db6ecab89a..2e70a8744c 100644 --- a/console/src/app/modules/password-complexity-view/password-complexity-view.component.ts +++ b/console/src/app/modules/password-complexity-view/password-complexity-view.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { AbstractControl } from '@angular/forms'; -import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; +import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb'; @Component({ selector: 'cnsl-password-complexity-view', @@ -9,5 +9,9 @@ import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy }) export class PasswordComplexityViewComponent { @Input() public password: AbstractControl | null = null; - @Input() public policy!: PasswordComplexityPolicy.AsObject; + @Input({ required: true }) public policy!: PasswordComplexityPolicy; + + protected get minLength() { + return Number(this.policy.minLength); + } } diff --git a/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.html b/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.html index 4c76a59f83..2e51fb3cc7 100644 --- a/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.html +++ b/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.html @@ -1,7 +1,6 @@ diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-create/oidc-webkeys-create.component.html b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-create/oidc-webkeys-create.component.html new file mode 100644 index 0000000000..c1494bcbd3 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-create/oidc-webkeys-create.component.html @@ -0,0 +1,60 @@ + + + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.KEY_TYPE' | translate }} + + RSA + ECDSA + ED25519 + + + +
+ + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.BITS' | translate }} + + + {{ + $any(bit.value).replace('RSA_BITS_', '') + }} + + + + + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.HASHER' | translate }} + + + {{ + $any(hasher.value).replace('RSA_HASHER_', '') + }} + + + + + +
+ + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.CURVE' | translate }} + + + {{ + $any(curve.value).replace('ECDSA_CURVE_', '') + }} + + + + + +
+ + + + + +
+
diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-create/oidc-webkeys-create.component.ts b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-create/oidc-webkeys-create.component.ts new file mode 100644 index 0000000000..a14231c7b1 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-create/oidc-webkeys-create.component.ts @@ -0,0 +1,60 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, signal, WritableSignal } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; +import { ReplaySubject } from 'rxjs'; +import { RSAHasher, RSABits, ECDSACurve } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; + +type RawValue = ReturnType; + +@Component({ + selector: 'cnsl-oidc-webkeys-create', + templateUrl: './oidc-webkeys-create.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OidcWebKeysCreateComponent { + protected readonly keyType: WritableSignal> = signal('rsa'); + protected readonly RSAHasher = RSAHasher; + protected readonly RSABits = RSABits; + protected readonly ECDSACurve = ECDSACurve; + protected readonly Number = Number; + protected readonly rsaForm = this.buildRsaForm(); + protected readonly ecdsaForm = this.buildEcdsaForm(); + protected readonly loading$ = new ReplaySubject(); + + @Output() + public readonly ngSubmit = new EventEmitter | RawValue | void>(); + + @Input() + public set loading(loading: boolean) { + this.loading$.next(loading); + } + + constructor(private readonly fb: FormBuilder) {} + + private buildRsaForm() { + return this.fb.group({ + bits: new FormControl(RSABits.RSA_BITS_2048, { + nonNullable: true, + validators: [Validators.required], + }), + hasher: new FormControl(RSAHasher.RSA_HASHER_SHA256, { + nonNullable: true, + validators: [Validators.required], + }), + }); + } + + private buildEcdsaForm() { + return this.fb.group({ + curve: new FormControl(ECDSACurve.ECDSA_CURVE_P256, { + nonNullable: true, + validators: [Validators.required], + }), + }); + } + + protected emitEd25519(event: SubmitEvent) { + event.preventDefault(); + this.ngSubmit.emit(); + } +} diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.html b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.html new file mode 100644 index 0000000000..2b270d32b1 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.html @@ -0,0 +1,40 @@ + + +
+ + + + + + + + + + + + + + + + +
+ {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.PREVIOUS_TABLE.DEACTIVATED_ON' | translate }} + + {{ row.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }} + {{ 'APP.PAGES.ID' | translate }} + {{ row.id }} + {{ 'PROJECT.TYPE.TITLE' | translate }} + {{ row.key.case | uppercase }} +
+
+
+
diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.ts b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.ts new file mode 100644 index 0000000000..b2236ae673 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; +import { WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; + +@Component({ + selector: 'cnsl-oidc-webkeys-inactive-table', + templateUrl: './oidc-webkeys-inactive-table.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OidcWebKeysInactiveTableComponent { + @Input({ required: true }) + public set inactiveWebKeys(webKeys: WebKey[] | null) { + this.inactiveWebKeys$.next(webKeys); + } + + private inactiveWebKeys$ = new ReplaySubject(1); + protected dataSource$ = this.inactiveWebKeys$.pipe( + filter(Boolean), + map((webKeys) => new MatTableDataSource(webKeys)), + ); +} diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.html b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.html new file mode 100644 index 0000000000..abaf656e2a --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.html @@ -0,0 +1,57 @@ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
{{ 'APP.PAGES.STATE' | translate }} + + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVE' | translate }} + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NEXT' | translate }} + {{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.FUTURE' | translate }} + + {{ 'APP.PAGES.ID' | translate }} + {{ row.id }} + {{ 'PROJECT.TYPE.TITLE' | translate }} + {{ row.key.case | uppercase }} + + + + +
+
+
diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.scss b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.scss new file mode 100644 index 0000000000..2ec90938a6 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.scss @@ -0,0 +1,4 @@ +.state.next { + color: #0e6245; + border: 1px solid #0e6245; +} diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.ts b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.ts new file mode 100644 index 0000000000..c0be5c5d56 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys-table/oidc-webkeys-table.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; +import { State, WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; + +@Component({ + selector: 'cnsl-oidc-webkeys-table', + templateUrl: './oidc-webkeys-table.component.html', + styleUrls: ['./oidc-webkeys-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OidcWebKeysTableComponent { + @Output() + public readonly refresh = new EventEmitter(); + + @Output() + public readonly delete = new EventEmitter(); + + @Input({ required: true }) + public set webKeys(webKeys: WebKey[] | null) { + this.webKeys$.next(webKeys); + } + + private readonly webKeys$ = new ReplaySubject(1); + protected readonly dataSource$ = this.webKeys$.pipe( + filter(Boolean), + map((keys) => new MatTableDataSource(keys)), + ); + protected readonly State = State; +} diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.html b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.html new file mode 100644 index 0000000000..b34ee9d30b --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.html @@ -0,0 +1,19 @@ +

{{ 'SETTINGS.LIST.WEB_KEYS' | translate }}

+

{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.DESCRIPTION' | translate }}

+

{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.TITLE' | translate }}

+

{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.DESCRIPTION' | translate }}

+

{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NOTE' | translate }}

+ + + + + diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.ts b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.ts new file mode 100644 index 0000000000..65197b4ad4 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.ts @@ -0,0 +1,217 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, signal } from '@angular/core'; +import { WebKeysService } from 'src/app/services/webkeys.service'; +import { defer, EMPTY, firstValueFrom, Observable, ObservedValueOf, of, shareReplay, Subject, switchMap } from 'rxjs'; +import { catchError, map, startWith, withLatestFrom } from 'rxjs/operators'; +import { ToastService } from 'src/app/services/toast.service'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { OidcWebKeysCreateComponent } from './oidc-webkeys-create/oidc-webkeys-create.component'; +import { TimestampToDatePipe } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe'; +import { MatDialog } from '@angular/material/dialog'; +import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { State, WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; +import { CreateWebKeyRequestSchema } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb'; +import { RSAHasher, RSABits, ECDSACurve } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; +import { NewFeatureService } from 'src/app/services/new-feature.service'; +import { ActivatedRoute, Router } from '@angular/router'; + +const CACHE_WARNING_MS = 5 * 60 * 1000; // 5 minutes + +@Component({ + selector: 'cnsl-oidc-webkeys', + templateUrl: './oidc-webkeys.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OidcWebKeysComponent implements OnInit { + protected readonly refresh = new Subject(); + protected readonly webKeysEnabled$: Observable; + protected readonly webKeys$: Observable; + protected readonly inactiveWebKeys$: Observable; + protected readonly nextWebKeyCandidate$: Observable; + + protected readonly activateLoading = signal(false); + protected readonly createLoading = signal(false); + + constructor( + private readonly webKeysService: WebKeysService, + private readonly featureService: NewFeatureService, + private readonly toast: ToastService, + private readonly timestampToDatePipe: TimestampToDatePipe, + private readonly dialog: MatDialog, + private readonly destroyRef: DestroyRef, + private readonly router: Router, + private readonly route: ActivatedRoute, + ) { + this.webKeysEnabled$ = this.getWebKeysEnabled().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + + const webKeys$ = this.getWebKeys(this.webKeysEnabled$).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + + this.webKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state !== State.INACTIVE))); + this.inactiveWebKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state === State.INACTIVE))); + + this.nextWebKeyCandidate$ = this.getNextWebKeyCandidate(this.webKeys$); + } + + ngOnInit(): void { + // redirect away from this page if web keys are not enabled + // this also preloads the web keys enabled state + this.webKeysEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (webKeysEnabled) => { + if (webKeysEnabled) { + return; + } + await this.router.navigate([], { + relativeTo: this.route, + queryParamsHandling: 'merge', + queryParams: { + id: null, + }, + }); + }); + } + + private getWebKeysEnabled() { + return defer(() => this.featureService.getInstanceFeatures()).pipe( + map((features) => features.webKey?.enabled ?? false), + catchError((err) => { + this.toast.showError(err); + return of(false); + }), + ); + } + + private getWebKeys(webKeysEnabled$: Observable) { + return this.refresh.pipe( + startWith(true), + switchMap(() => { + return this.webKeysService.ListWebKeys(); + }), + map(({ webKeys }) => webKeys), + catchError(async (err) => { + const webKeysEnabled = await firstValueFrom(webKeysEnabled$); + // suppress errors if web keys are not enabled + if (!webKeysEnabled) { + return []; + } + + this.toast.showError(err); + return []; + }), + ); + } + + private getNextWebKeyCandidate(webKeys$: Observable) { + return webKeys$.pipe( + map((webKeys) => { + if (webKeys.length < 2) { + return undefined; + } + const [webKey, nextWebKey] = webKeys; + if (webKey.state !== State.ACTIVE) { + return undefined; + } + if (nextWebKey.state !== State.INITIAL) { + return undefined; + } + return nextWebKey; + }), + ); + } + + protected async createWebKey(event: ObservedValueOf) { + try { + this.createLoading.set(true); + + const req = !event + ? this.createEd25519() + : 'curve' in event + ? this.createEcdsa(event.curve) + : this.createRsa(event.bits, event.hasher); + await this.webKeysService.CreateWebKey(req); + + this.refresh.next(true); + } catch (error) { + this.toast.showError(error); + } finally { + this.createLoading.set(false); + } + } + + private createEd25519(): MessageInitShape { + return { + key: { + case: 'ed25519', + value: {}, + }, + }; + } + + private createEcdsa(curve: ECDSACurve): MessageInitShape { + return { + key: { + case: 'ecdsa', + value: { + curve, + }, + }, + }; + } + + private createRsa(bits: RSABits, hasher: RSAHasher): MessageInitShape { + return { + key: { + case: 'rsa', + value: { + bits, + hasher, + }, + }, + }; + } + + protected async deleteWebKey(row: WebKey) { + try { + await this.webKeysService.DeleteWebKey(row.id); + this.refresh.next(true); + } catch (err) { + this.toast.showError(err); + } + } + + protected async activateWebKey(nextWebKey: WebKey) { + try { + this.activateLoading.set(true); + const creationDate = this.timestampToDatePipe.transform(nextWebKey.creationDate); + if (!creationDate) { + // noinspection ExceptionCaughtLocallyJS + throw new Error('Invalid creation date'); + } + + const diffToCurrentTime = Date.now() - creationDate.getTime(); + if (diffToCurrentTime < CACHE_WARNING_MS && !(await this.openCacheWarnDialog())) { + return; + } + + await this.webKeysService.ActivateWebKey(nextWebKey.id); + this.refresh.next(true); + } catch (error) { + this.toast.showError(error); + } finally { + this.activateLoading.set(false); + } + } + + private openCacheWarnDialog() { + const dialogRef = this.dialog.open(WarnDialogComponent, { + data: { + confirmKey: 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVATE', + cancelKey: 'ACTIONS.CANCEL', + titleKey: 'Web Key is less then 5 min old', + descriptionKey: 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NOTE', + }, + width: '400px', + }); + + const obs = dialogRef.afterClosed().pipe(map(Boolean), takeUntilDestroyed(this.destroyRef)); + return firstValueFrom(obs); + } +} diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.module.ts b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.module.ts new file mode 100644 index 0000000000..6d54f9b314 --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.module.ts @@ -0,0 +1,58 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { OidcWebKeysComponent } from './oidc-webkeys.component'; +import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatTableModule } from '@angular/material/table'; +import { MatMenuModule } from '@angular/material/menu'; +import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module'; +import { MatButtonModule } from '@angular/material/button'; +import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module'; +import { MatIconModule } from '@angular/material/icon'; +import { FormFieldModule } from 'src/app/modules/form-field/form-field.module'; +import { MatSelectModule } from '@angular/material/select'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CardModule } from 'src/app/modules/card/card.module'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { OidcWebKeysCreateComponent } from './oidc-webkeys-create/oidc-webkeys-create.component'; +import { OidcWebKeysTableComponent } from './oidc-webkeys-table/oidc-webkeys-table.component'; +import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; +import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; +import { OidcWebKeysInactiveTableComponent } from './oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component'; +import { TypeSafeCellDefDirective } from './type-safe-cell-def.directive'; +import { TimestampToDatePipe } from '../../../pipes/timestamp-to-date-pipe/timestamp-to-date.pipe'; +import { MatTooltipModule } from '@angular/material/tooltip'; + +@NgModule({ + declarations: [ + OidcWebKeysComponent, + OidcWebKeysCreateComponent, + OidcWebKeysTableComponent, + OidcWebKeysInactiveTableComponent, + TypeSafeCellDefDirective, + ], + providers: [TimestampToDatePipe], + imports: [ + CommonModule, + TranslateModule, + RefreshTableModule, + MatCheckboxModule, + MatTableModule, + MatMenuModule, + TableActionsModule, + MatButtonModule, + ActionKeysModule, + MatIconModule, + FormFieldModule, + MatSelectModule, + ReactiveFormsModule, + CardModule, + MatProgressSpinnerModule, + TimestampToDatePipeModule, + LocalizedDatePipeModule, + MatTooltipModule, + ], + exports: [OidcWebKeysComponent], +}) +export class OidcWebkeysModule {} diff --git a/console/src/app/modules/policies/oidc-webkeys/type-safe-cell-def.directive.ts b/console/src/app/modules/policies/oidc-webkeys/type-safe-cell-def.directive.ts new file mode 100644 index 0000000000..a3a145964b --- /dev/null +++ b/console/src/app/modules/policies/oidc-webkeys/type-safe-cell-def.directive.ts @@ -0,0 +1,16 @@ +import { Directive, Input } from '@angular/core'; +import { DataSource } from '@angular/cdk/collections'; +import { MatCellDef } from '@angular/material/table'; +import { CdkCellDef } from '@angular/cdk/table'; + +@Directive({ + selector: '[cnslCellDef]', + providers: [{ provide: CdkCellDef, useExisting: TypeSafeCellDefDirective }], +}) +export class TypeSafeCellDefDirective extends MatCellDef { + @Input({ required: true }) cnslCellDefDataSource!: DataSource; + + static ngTemplateContextGuard(_dir: TypeSafeCellDefDirective, _ctx: any): _ctx is { $implicit: T; index: number } { + return true; + } +} diff --git a/console/src/app/modules/policies/private-labeling-policy/color/color.component.ts b/console/src/app/modules/policies/private-labeling-policy/color/color.component.ts index 94f7d6a064..f7cd38f810 100644 --- a/console/src/app/modules/policies/private-labeling-policy/color/color.component.ts +++ b/console/src/app/modules/policies/private-labeling-policy/color/color.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { ColorEvent } from 'ngx-color'; +import type { ColorEvent } from 'ngx-color'; import { BehaviorSubject, debounceTime } from 'rxjs'; import { ColorType } from '../private-labeling-policy.component'; diff --git a/console/src/app/modules/project-roles-table/project-roles-table.component.html b/console/src/app/modules/project-roles-table/project-roles-table.component.html index 48ad2851bb..56e737074d 100644 --- a/console/src/app/modules/project-roles-table/project-roles-table.component.html +++ b/console/src/app/modules/project-roles-table/project-roles-table.component.html @@ -2,7 +2,6 @@ [showSelectionActionButton]="showSelectionActionButton" *ngIf="projectId" (refreshed)="refreshPage()" - [dataSize]="dataSource.totalResult" [emitRefreshOnPreviousRoutes]="['/projects/' + projectId + '/roles/create']" [selection]="selection" [loading]="dataSource.loading$ | async" diff --git a/console/src/app/modules/providers/provider-oauth/provider-oauth.component.html b/console/src/app/modules/providers/provider-oauth/provider-oauth.component.html index b8f43a856a..8c6412cd91 100644 --- a/console/src/app/modules/providers/provider-oauth/provider-oauth.component.html +++ b/console/src/app/modules/providers/provider-oauth/provider-oauth.component.html @@ -110,6 +110,15 @@
+
+ +
+

{{ 'IDP.USEPKCE_DESC' | translate }}

+ {{ 'IDP.USEPKCE' | translate }} +
+
+
+
-
+

{{ 'IDP.ISIDTOKENMAPPING_DESC' | translate }}

{{ 'IDP.ISIDTOKENMAPPING' | translate }}
+ + +
+

{{ 'IDP.USEPKCE_DESC' | translate }}

+ {{ 'IDP.USEPKCE' | translate }} +
+
+ { @@ -165,6 +166,7 @@ export class ProviderOIDCComponent { req.setScopesList(this.scopesList?.value); req.setProviderOptions(this.options); req.setIsIdTokenMapping(this.isIdTokenMapping?.value); + req.setUsePkce(this.usePkce?.value); this.loading = true; this.service @@ -193,11 +195,12 @@ export class ProviderOIDCComponent { req.setScopesList(this.scopesList?.value); req.setProviderOptions(this.options); req.setIsIdTokenMapping(this.isIdTokenMapping?.value); + req.setUsePkce(this.usePkce?.value); this.loading = true; this.service .updateGenericOIDCProvider(req) - .then((idp) => { + .then(() => { setTimeout(() => { this.loading = false; this.close(); @@ -261,4 +264,8 @@ export class ProviderOIDCComponent { public get isIdTokenMapping(): AbstractControl | null { return this.form.get('isIdTokenMapping'); } + + public get usePkce(): AbstractControl | null { + return this.form.get('usePkce'); + } } diff --git a/console/src/app/modules/refresh-table/refresh-table.component.ts b/console/src/app/modules/refresh-table/refresh-table.component.ts index 6c6d46e281..7e8ada9faf 100644 --- a/console/src/app/modules/refresh-table/refresh-table.component.ts +++ b/console/src/app/modules/refresh-table/refresh-table.component.ts @@ -3,6 +3,7 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { RefreshService } from 'src/app/services/refresh.service'; +import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt'; import { ActionKeysType } from '../action-keys/action-keys.component'; @@ -27,8 +28,7 @@ const rotate = animation([ }) export class RefreshTableComponent implements OnInit { @Input() public selection: SelectionModel = new SelectionModel(true, []); - @Input() public timestamp: Timestamp.AsObject | undefined = undefined; - @Input() public dataSize: number = 0; + @Input() public timestamp: Timestamp.AsObject | ConnectTimestamp | undefined = undefined; @Input() public emitRefreshAfterTimeoutInMs: number = 0; @Input() public loading: boolean | null = false; @Input() public emitRefreshOnPreviousRoutes: string[] = []; diff --git a/console/src/app/modules/settings-list/settings-list.component.html b/console/src/app/modules/settings-list/settings-list.component.html index 0392a03849..728125a891 100644 --- a/console/src/app/modules/settings-list/settings-list.component.html +++ b/console/src/app/modules/settings-list/settings-list.component.html @@ -1,82 +1,85 @@ - - + +

{{ 'ORG.PAGES.LIST' | translate }}

{{ 'ORG.PAGES.LISTDESCRIPTION' | translate }}

- + - + - + - + - + - + - + - + - + - + - + - + - + + + + - + - + - + - + - + - + - + - + - + + + + + + + + +
diff --git a/console/src/app/modules/settings-list/settings-list.component.ts b/console/src/app/modules/settings-list/settings-list.component.ts index 02d7b1fd13..a39b23df95 100644 --- a/console/src/app/modules/settings-list/settings-list.component.ts +++ b/console/src/app/modules/settings-list/settings-list.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Component, effect, Input, OnInit, signal } from '@angular/core'; import { PolicyComponentServiceType } from '../policies/policy-component-types.enum'; import { SidenavSetting } from '../sidenav/sidenav.component'; @@ -8,28 +8,40 @@ import { SidenavSetting } from '../sidenav/sidenav.component'; templateUrl: './settings-list.component.html', styleUrls: ['./settings-list.component.scss'], }) -export class SettingsListComponent implements OnChanges, OnInit { - @Input() public title: string = ''; - @Input() public description: string = ''; - @Input() public serviceType!: PolicyComponentServiceType; - @Input() public selectedId: string | undefined = undefined; - @Input() public settingsList: SidenavSetting[] = []; - public currentSetting: string | undefined = ''; - public PolicyComponentServiceType: any = PolicyComponentServiceType; - constructor() {} +export class SettingsListComponent implements OnInit { + @Input({ required: true }) public serviceType!: PolicyComponentServiceType; + @Input() public set selectedId(selectedId: string) { + this.selectedId$.set(selectedId); + } + @Input({ required: true }) public settingsList: SidenavSetting[] = []; - ngOnChanges(changes: SimpleChanges): void { - if (this.settingsList && this.settingsList.length && changes['selectedId']?.currentValue) { - this.currentSetting = - this.settingsList && this.settingsList.find((l) => l.id === changes['selectedId'].currentValue) - ? changes['selectedId'].currentValue - : ''; - } + protected setting = signal(null); + private selectedId$ = signal(undefined); + protected PolicyComponentServiceType: any = PolicyComponentServiceType; + + constructor() { + effect( + () => { + const selectedId = this.selectedId$(); + if (!selectedId) { + return; + } + + const setting = this.settingsList.find(({ id }) => id === selectedId); + if (!setting) { + return; + } + this.setting.set(setting); + }, + { allowSignalWrites: true }, + ); } ngOnInit(): void { - if (!this.currentSetting) { - this.currentSetting = this.settingsList ? this.settingsList[0].id : ''; + const firstSetting = this.settingsList[0]; + if (!firstSetting || this.setting()) { + return; } + this.setting.set(firstSetting); } } diff --git a/console/src/app/modules/settings-list/settings-list.module.ts b/console/src/app/modules/settings-list/settings-list.module.ts index 3ff71f9e30..c6abe310c0 100644 --- a/console/src/app/modules/settings-list/settings-list.module.ts +++ b/console/src/app/modules/settings-list/settings-list.module.ts @@ -31,9 +31,13 @@ import { OrgTableModule } from '../org-table/org-table.module'; import { NotificationSMTPProviderModule } from '../policies/notification-smtp-provider/notification-smtp-provider.module'; import { FeaturesComponent } from 'src/app/components/features/features.component'; import OrgListModule from 'src/app/pages/org-list/org-list.module'; +import ActionsTwoModule from '../actions-two/actions-two.module'; +import { provideRouter } from '@angular/router'; +import { OidcWebkeysModule } from '../policies/oidc-webkeys/oidc-webkeys.module'; @NgModule({ declarations: [SettingsListComponent], + providers: [provideRouter([])], imports: [ CommonModule, FormsModule, @@ -62,10 +66,12 @@ import OrgListModule from 'src/app/pages/org-list/org-list.module'; NotificationSMTPProviderModule, NotificationSMSProviderModule, OIDCConfigurationModule, + OidcWebkeysModule, SecretGeneratorModule, FailedEventsModule, IamViewsModule, EventsModule, + ActionsTwoModule, ], exports: [SettingsListComponent], }) diff --git a/console/src/app/modules/settings-list/settings.ts b/console/src/app/modules/settings-list/settings.ts index 0335e511b2..79b92e2214 100644 --- a/console/src/app/modules/settings-list/settings.ts +++ b/console/src/app/modules/settings-list/settings.ts @@ -35,6 +35,14 @@ export const OIDC: SidenavSetting = { }, }; +export const WEBKEYS: SidenavSetting = { + id: 'webkeys', + i18nKey: 'SETTINGS.LIST.WEB_KEYS', + requiredRoles: { + [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + }, +}; + export const SECRETS: SidenavSetting = { id: 'secrets', i18nKey: 'SETTINGS.LIST.SECRETS', @@ -214,3 +222,23 @@ export const BRANDING: SidenavSetting = { [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], }, }; + +export const ACTIONS: SidenavSetting = { + id: 'actions', + i18nKey: 'SETTINGS.LIST.ACTIONS', + groupI18nKey: 'SETTINGS.GROUPS.ACTIONS', + requiredRoles: { + // todo: figure out roles + [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + }, +}; + +export const ACTIONS_TARGETS: SidenavSetting = { + id: 'actions_targets', + i18nKey: 'SETTINGS.LIST.TARGETS', + groupI18nKey: 'SETTINGS.GROUPS.ACTIONS', + requiredRoles: { + // todo: figure out roles + [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + }, +}; diff --git a/console/src/app/modules/sidenav/sidenav.component.html b/console/src/app/modules/sidenav/sidenav.component.html index d12a3e1c24..277686852b 100644 --- a/console/src/app/modules/sidenav/sidenav.component.html +++ b/console/src/app/modules/sidenav/sidenav.component.html @@ -1,12 +1,9 @@
-

{{ title }}

-

{{ description }}

- - - - {{ setting.groupI18nKey | translate }} + + {{ setting.groupI18nKey | translate }} - - - - - +
diff --git a/console/src/app/modules/sidenav/sidenav.component.ts b/console/src/app/modules/sidenav/sidenav.component.ts index dc7948a256..33539750a2 100644 --- a/console/src/app/modules/sidenav/sidenav.component.ts +++ b/console/src/app/modules/sidenav/sidenav.component.ts @@ -1,7 +1,5 @@ -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; - import { PolicyComponentServiceType } from '../policies/policy-component-types.enum'; export interface SidenavSetting { @@ -19,59 +17,61 @@ export interface SidenavSetting { selector: 'cnsl-sidenav', templateUrl: './sidenav.component.html', styleUrls: ['./sidenav.component.scss'], - providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SidenavComponent), multi: true }], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SidenavComponent implements ControlValueAccessor { - @Input() public title: string = ''; - @Input() public description: string = ''; +export class SidenavComponent { + @Input() public navigate: boolean = true; @Input() public indented: boolean = false; - @Input() public currentSetting?: string | undefined = undefined; - @Input() public settingsList: SidenavSetting[] = []; - @Input() public queryParam: string = ''; + @Input({ required: true }) public settingsList: SidenavSetting[] = []; + @Input({ required: true }) + public set setting(setting: SidenavSetting | null) { + if (!setting) { + return; + } + this.setting$.set(setting); + } + + @Output() + public settingChange = new EventEmitter(); + + protected readonly setting$ = signal(null); + + protected PolicyComponentServiceType = PolicyComponentServiceType; - public PolicyComponentServiceType: any = PolicyComponentServiceType; constructor( - private router: Router, - private route: ActivatedRoute, - ) {} + private readonly router: Router, + private readonly route: ActivatedRoute, + ) { + effect( + () => { + const setting = this.setting$(); + if (setting === null) { + return; + } - private onChange = (current: string | undefined) => {}; - private onTouch = (current: string | undefined) => {}; + this.settingChange.emit(setting); - @Input() get value(): string | undefined { - return this.currentSetting; + if (!this.navigate) { + return; + } + + this.router + .navigate([], { + relativeTo: this.route, + queryParams: { + id: setting ? setting.id : undefined, + }, + replaceUrl: true, + queryParamsHandling: 'merge', + skipLocationChange: false, + }) + .then(); + }, + { allowSignalWrites: true }, + ); } - set value(setting: string | undefined) { - this.currentSetting = setting; - - if (setting || setting === undefined || setting === '') { - this.onChange(setting); - this.onTouch(setting); - } - - if (this.queryParam && setting) { - this.router.navigate([], { - relativeTo: this.route, - queryParams: { - [this.queryParam]: setting, - }, - replaceUrl: true, - queryParamsHandling: 'merge', - skipLocationChange: false, - }); - } - } - - public writeValue(value: any) { - this.value = value; - } - - public registerOnChange(fn: any) { - this.onChange = fn; - } - - public registerOnTouched(fn: any) { - this.onTouch = fn; + protected trackSettings(_: number, setting: SidenavSetting): string { + return setting.id; } } diff --git a/console/src/app/modules/smtp-table/smtp-table.component.html b/console/src/app/modules/smtp-table/smtp-table.component.html index b71a7fcefd..3143aee793 100644 --- a/console/src/app/modules/smtp-table/smtp-table.component.html +++ b/console/src/app/modules/smtp-table/smtp-table.component.html @@ -1,7 +1,6 @@
-
+
- - @@ -80,8 +75,8 @@
- - + +
@@ -239,6 +234,26 @@ >
+ +
+
+ +
+ diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss new file mode 100644 index 0000000000..7a29364fc3 --- /dev/null +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss @@ -0,0 +1,57 @@ +.content { + max-width: 45rem; + + @media only screen and (max-width: 500px) { + padding: 0 0.5rem; + } +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + grid-template-areas: + 'email email' + 'emailVerified emailVerified' + 'username username' + 'givenName familyName' + 'authenticationFactor authenticationFactor'; + column-gap: 1rem; +} + +.email { + grid-area: email; +} + +.emailVerified { + grid-area: emailVerified; +} + +.givenName { + grid-area: givenName; +} + +.familyName { + grid-area: familyName; +} + +.username { + grid-area: username; +} + +.authenticationFactor { + grid-area: authenticationFactor; + margin-bottom: 1rem; +} + +.authenticationFactorRadioGroup > mat-radio-button { + display: block; +} + +.authenticationFactorButton { + margin-top: 1rem; +} + +.stretchInput { + max-width: unset; +} diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts new file mode 100644 index 0000000000..9fd765264d --- /dev/null +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts @@ -0,0 +1,257 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, signal } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ToastService } from 'src/app/services/toast.service'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { UserService } from 'src/app/services/user.service'; +import { Location } from '@angular/common'; +import { + emailValidator, + minLengthValidator, + passwordConfirmValidator, + requiredValidator, +} from 'src/app/modules/form-field/validators/validators'; +import { NewMgmtService } from 'src/app/services/new-mgmt.service'; +import { + defaultIfEmpty, + defer, + EMPTY, + firstValueFrom, + mergeWith, + NEVER, + Observable, + of, + shareReplay, + TimeoutError, +} from 'rxjs'; +import { catchError, filter, map, startWith, timeout } from 'rxjs/operators'; +import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { AddHumanUserRequestSchema } from '@zitadel/proto/zitadel/user/v2/user_service_pb'; +import { LoginV2FeatureFlag } from '@zitadel/proto/zitadel/feature/v2/feature_pb'; +import { withLatestFromSynchronousFix } from 'src/app/utils/withLatestFromSynchronousFix'; +import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service'; +import { NewFeatureService } from 'src/app/services/new-feature.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +type PwdForm = ReturnType; +type AuthenticationFactor = + | { factor: 'none' } + | { factor: 'initialPassword'; form: PwdForm; policy: PasswordComplexityPolicy } + | { factor: 'invitation' }; + +@Component({ + selector: 'cnsl-user-create-v2', + templateUrl: './user-create-v2.component.html', + styleUrls: ['./user-create-v2.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UserCreateV2Component implements OnInit { + protected readonly loading = signal(false); + + protected readonly userForm: ReturnType; + + private readonly passwordComplexityPolicy$: Observable; + protected readonly authenticationFactor$: Observable; + private readonly useLoginV2$: Observable; + + constructor( + private readonly router: Router, + private readonly toast: ToastService, + private readonly fb: FormBuilder, + private readonly userService: UserService, + private readonly newMgmtService: NewMgmtService, + private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService, + private readonly featureService: NewFeatureService, + private readonly destroyRef: DestroyRef, + private readonly route: ActivatedRoute, + protected readonly location: Location, + ) { + this.userForm = this.buildUserForm(); + + this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.authenticationFactor$ = this.getAuthenticationFactor(this.userForm, this.passwordComplexityPolicy$); + this.useLoginV2$ = this.getUseLoginV2().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + } + + ngOnInit(): void { + this.useLoginV2$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); + this.authenticationFactor$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async ({ factor }) => { + // preserve current factor choice when reloading helpful while developing + await this.router.navigate([], { + relativeTo: this.route, + queryParams: { + factor, + }, + queryParamsHandling: 'merge', + }); + }); + } + + public buildUserForm() { + const param = this.route.snapshot.queryParamMap.get('factor'); + const authenticationFactor = + param === 'none' ? param : param === 'initialPassword' ? param : param === 'invitation' ? param : 'none'; + + return this.fb.group({ + email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }), + username: new FormControl('', { nonNullable: true, validators: [requiredValidator, minLengthValidator(2)] }), + givenName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + familyName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + emailVerified: new FormControl(false, { nonNullable: true }), + authenticationFactor: new FormControl(authenticationFactor, { + nonNullable: true, + }), + }); + } + + private getPasswordComplexityPolicy() { + return defer(() => this.newMgmtService.getPasswordComplexityPolicy()).pipe( + map(({ policy }) => policy), + filter(Boolean), + catchError((error) => { + this.toast.showError(error); + return EMPTY; + }), + ); + } + + private getAuthenticationFactor( + userForm: typeof this.userForm, + passwordComplexityPolicy$: Observable, + ): Observable { + const pwdForm$ = passwordComplexityPolicy$.pipe( + defaultIfEmpty(undefined), + map((policy) => this.buildPwdForm(policy)), + ); + + return userForm.controls.authenticationFactor.valueChanges.pipe( + startWith(userForm.controls.authenticationFactor.value), + withLatestFromSynchronousFix(pwdForm$, passwordComplexityPolicy$), + map(([factor, form, policy]) => { + if (factor === 'initialPassword') { + return { factor, form, policy }; + } + // reset emailVerified when we switch to invitation + if (factor === 'invitation') { + userForm.controls.emailVerified.setValue(false); + } + return { factor }; + }), + ); + } + + private buildPwdForm(policy: PasswordComplexityPolicy | undefined) { + return this.fb.group({ + password: new FormControl('', { + nonNullable: true, + validators: this.passwordComplexityValidatorFactory.buildValidators(policy), + }), + confirmPassword: new FormControl('', { + nonNullable: true, + validators: [requiredValidator, passwordConfirmValidator()], + }), + }); + } + + private getUseLoginV2() { + return defer(() => this.featureService.getInstanceFeatures()).pipe( + map(({ loginV2 }) => loginV2), + timeout(1000), + catchError((err) => { + if (!(err instanceof TimeoutError)) { + this.toast.showError(err); + } + return of(undefined); + }), + mergeWith(NEVER), + ); + } + + protected async createUserV2(authenticationFactor: AuthenticationFactor) { + try { + await this.createUserV2Try(authenticationFactor); + } catch (error) { + this.toast.showError(error); + } finally { + this.loading.set(false); + } + } + + private async createUserV2Try(authenticationFactor: AuthenticationFactor) { + this.loading.set(true); + + const userValues = this.userForm.getRawValue(); + + const humanReq: MessageInitShape = { + username: userValues.username, + profile: { + givenName: userValues.givenName, + familyName: userValues.familyName, + }, + email: { + email: userValues.email, + verification: { + case: 'isVerified', + value: userValues.emailVerified, + }, + }, + }; + + if (authenticationFactor.factor === 'initialPassword') { + const { password } = authenticationFactor.form.getRawValue(); + humanReq.passwordType = { + case: 'password', + value: { + password, + }, + }; + } + + const resp = await this.userService.addHumanUser(humanReq); + if (authenticationFactor.factor === 'invitation') { + const url = await this.getUrlTemplate(); + await this.userService.createInviteCode({ + userId: resp.userId, + verification: { + case: 'sendCode', + value: url + ? { + urlTemplate: `${url}verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, + } + : {}, + }, + }); + } + + this.toast.showInfo('USER.TOAST.CREATED', true); + await this.router.navigate(['users', resp.userId], { queryParams: { new: true } }); + } + + private async getUrlTemplate() { + const useLoginV2 = await firstValueFrom(this.useLoginV2$); + if (!useLoginV2?.required) { + // loginV2 is not enabled + return undefined; + } + + const { baseUri } = useLoginV2; + // if base uri is not set, we use the default for the cloud hosted login v2 + if (!baseUri) { + return new URL(location.origin + '/ui/v2/login/'); + } + + const baseUriWithTrailingSlash = baseUri.endsWith('/') ? baseUri : `${baseUri}/`; + try { + // first we try to create a URL directly from the baseUri + return new URL(baseUriWithTrailingSlash); + } catch (_) { + // if this does not work we assume that the baseUri is relative, + // and we need to add the location.origin + // we make sure the relative url has a slash at the beginning and end + const baseUriWithSlashes = baseUriWithTrailingSlash.startsWith('/') + ? baseUriWithTrailingSlash + : `/${baseUriWithTrailingSlash}`; + return new URL(location.origin + baseUriWithSlashes); + } + } +} diff --git a/console/src/app/pages/users/user-create/user-create.component.html b/console/src/app/pages/users/user-create/user-create.component.html index 94a42cdc98..1b65cc992e 100644 --- a/console/src/app/pages/users/user-create/user-create.component.html +++ b/console/src/app/pages/users/user-create/user-create.component.html @@ -1,13 +1,20 @@ +
-
+

{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}

@@ -19,12 +26,13 @@ {{ 'USER.PROFILE.USERNAME' | translate }} - {{ envSuffix }} + {{ envSuffix }} @@ -42,7 +50,7 @@
-
- +
+
- + {{ 'USER.PASSWORD.NEWINITIAL' | translate }} - + {{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }} = new Subject(); + protected readonly countryPhoneCodes: CountryPhoneCode[]; - public userLoginMustBeDomain: boolean = false; - public loading: boolean = false; + protected loading = false; - @ViewChild('suffix') public suffix!: any; - private primaryDomain!: Domain.AsObject; - public usePassword: boolean = false; - public policy!: PasswordComplexityPolicy.AsObject; + private readonly suffix$ = new ReplaySubject(1); + @ViewChild('suffix') public set suffix(suffix: ElementRef | undefined) { + if (suffix?.nativeElement) { + this.suffix$.next(suffix.nativeElement); + } + } + + protected usePassword: boolean = false; + protected readonly envSuffix$: Observable; + protected readonly userForm: ReturnType; + protected readonly pwdForm$: ReturnType; + protected readonly passwordComplexityPolicy$: Observable; + protected readonly useV2Api$: Observable; + protected readonly suffixPadding$: Observable; constructor( - private router: Router, - private toast: ToastService, - private fb: UntypedFormBuilder, - private mgmtService: ManagementService, - private changeDetRef: ChangeDetectorRef, - private _location: Location, - private countryCallingCodesService: CountryCallingCodesService, - public langSvc: LanguagesService, - breadcrumbService: BreadcrumbService, + private readonly router: Router, + private readonly toast: ToastService, + private readonly fb: FormBuilder, + private readonly mgmtService: ManagementService, + private readonly newMgmtService: NewMgmtService, + private readonly destroyRef: DestroyRef, + private readonly breadcrumbService: BreadcrumbService, + protected readonly location: Location, + protected readonly langSvc: LanguagesService, + private readonly featureService: NewFeatureService, + private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService, + countryCallingCodesService: CountryCallingCodesService, ) { - breadcrumbService.setBreadcrumb([ + this.envSuffix$ = this.getEnvSuffix(); + this.suffixPadding$ = this.getSuffixPadding(); + this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.useV2Api$ = this.getUseV2Api().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + + this.userForm = this.buildUserForm(); + this.pwdForm$ = this.buildPwdForm(this.passwordComplexityPolicy$); + + this.countryPhoneCodes = countryCallingCodesService.getCountryCallingCodes(); + + this.breadcrumbService.setBreadcrumb([ new Breadcrumb({ type: BreadcrumbType.ORG, routerLink: ['/org'], }), ]); - this.loading = true; - this.loadOrg(); - this.mgmtService - .getDomainPolicy() - .then((resp) => { - if (resp.policy?.userLoginMustBeDomain) { - this.userLoginMustBeDomain = resp.policy.userLoginMustBeDomain; + } + + ngOnInit(): void { + this.watchPhoneChanges(); + } + + private getEnvSuffix() { + const domainPolicy$ = defer(() => this.mgmtService.getDomainPolicy()); + const orgDomains$ = defer(() => this.mgmtService.listOrgDomains()); + + return forkJoin([domainPolicy$, orgDomains$]).pipe( + map(([policy, domains]) => { + const userLoginMustBeDomain = policy.policy?.userLoginMustBeDomain; + const primaryDomain = domains.resultList.find((resp) => resp.isPrimary); + if (userLoginMustBeDomain && primaryDomain) { + return `@${primaryDomain.domainName}`; + } else { + return ''; } - this.initForm(); - this.loading = false; - this.changeDetRef.detectChanges(); - }) - .catch((error) => { - console.error(error); - this.initForm(); - this.loading = false; - this.changeDetRef.detectChanges(); - }); + }), + catchError(() => of('')), + ); } - public close(): void { - this._location.back(); + private getSuffixPadding() { + return this.suffix$.pipe( + map((suffix) => `${suffix.offsetWidth + 10}px`), + startWith('10px'), + ); } - private async loadOrg(): Promise { - const domains = await this.mgmtService.listOrgDomains(); - const found = domains.resultList.find((resp) => resp.isPrimary); - if (found) { - this.primaryDomain = found; - } + private getPasswordComplexityPolicy() { + return defer(() => this.newMgmtService.getPasswordComplexityPolicy()).pipe( + map(({ policy }) => policy), + catchError((error) => { + this.toast.showError(error); + return EMPTY; + }), + ); } - private initForm(): void { - this.userForm = this.fb.group({ - email: ['', [requiredValidator, emailValidator]], - userName: ['', [requiredValidator, minLengthValidator(2)]], - firstName: ['', requiredValidator], - lastName: ['', requiredValidator], - nickName: [''], - gender: [], - preferredLanguage: [''], - phone: ['', phoneValidator], - isVerified: [false, []], + private getUseV2Api() { + return defer(() => this.featureService.getInstanceFeatures()).pipe( + map((features) => features.consoleUseV2UserApi?.enabled ?? false), + timeout(1000), + catchError((err) => { + if (!(err instanceof TimeoutError)) { + this.toast.showError(err); + } + return of(false); + }), + ); + } + + private buildUserForm() { + return this.fb.group({ + email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }), + userName: new FormControl('', { nonNullable: true, validators: [requiredValidator, minLengthValidator(2)] }), + firstName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + lastName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + nickName: new FormControl('', { nonNullable: true }), + gender: new FormControl(Gender.GENDER_UNSPECIFIED, { nonNullable: true, validators: [requiredValidator] }), + preferredLanguage: new FormControl('', { nonNullable: true }), + phone: new FormControl('', { nonNullable: true, validators: [phoneValidator] }), + emailVerified: new FormControl(false, { nonNullable: true }), }); + } - const validators: Validators[] = [requiredValidator]; - - this.mgmtService.getPasswordComplexityPolicy().then((data) => { - if (data.policy) { - this.policy = data.policy; - - if (this.policy.minLength) { - validators.push(minLengthValidator(this.policy.minLength)); - } - if (this.policy.hasLowercase) { - validators.push(containsLowerCaseValidator); - } - if (this.policy.hasUppercase) { - validators.push(containsUpperCaseValidator); - } - if (this.policy.hasNumber) { - validators.push(containsNumberValidator); - } - if (this.policy.hasSymbol) { - validators.push(containsSymbolValidator); - } - const pwdValidators = [...validators] as ValidatorFn[]; - const confirmPwdValidators = [requiredValidator, passwordConfirmValidator()] as ValidatorFn[]; - - this.pwdForm = this.fb.group({ - password: ['', pwdValidators], - confirmPassword: ['', confirmPwdValidators], + private buildPwdForm(passwordComplexityPolicy$: Observable) { + return passwordComplexityPolicy$.pipe( + map((policy) => { + return this.fb.group({ + password: new FormControl('', { + nonNullable: true, + validators: this.passwordComplexityValidatorFactory.buildValidators(policy), + }), + confirmPassword: new FormControl('', { + nonNullable: true, + validators: [requiredValidator, passwordConfirmValidator()], + }), }); - } - }); + }), + ); + } - this.phone?.valueChanges.pipe(debounceTime(200)).subscribe((value: string) => { + private watchPhoneChanges(): void { + const phone = this.userForm.controls.phone; + + phone.valueChanges.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe((value) => { const phoneNumber = formatPhone(value); if (phoneNumber) { this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country); - this.phone?.setValue(phoneNumber.phone); + phone.setValue(phoneNumber.phone); } }); } - public createUser(): void { - this.user = this.userForm.value; - + protected async createUser(pwdForm: ObservedValueOf): Promise { this.loading = true; + const controls = this.userForm.controls; const profileReq = new AddHumanUserRequest.Profile(); - profileReq.setFirstName(this.firstName?.value); - profileReq.setLastName(this.lastName?.value); - profileReq.setNickName(this.nickName?.value); - profileReq.setPreferredLanguage(this.preferredLanguage?.value); - profileReq.setGender(this.gender?.value); + profileReq.setFirstName(controls.firstName.value); + profileReq.setLastName(controls.lastName.value); + profileReq.setNickName(controls.nickName.value); + profileReq.setPreferredLanguage(controls.preferredLanguage.value); + profileReq.setGender(controls.gender.value); const humanReq = new AddHumanUserRequest(); - humanReq.setUserName(this.userName?.value); + humanReq.setUserName(controls.userName.value); humanReq.setProfile(profileReq); const emailreq = new AddHumanUserRequest.Email(); - emailreq.setEmail(this.email?.value); - emailreq.setIsEmailVerified(this.isVerified?.value); + emailreq.setEmail(controls.email.value); + emailreq.setIsEmailVerified(controls.emailVerified.value); humanReq.setEmail(emailreq); - if (this.usePassword && this.password?.value) { - humanReq.setInitialPassword(this.password.value); + if (this.usePassword) { + humanReq.setInitialPassword(pwdForm.controls.password.value); } - if (this.phone && this.phone.value) { + if (controls.phone.value) { // Try to parse number and format it according to country - const phoneNumber = formatPhone(this.phone.value); + const phoneNumber = formatPhone(controls.phone.value); if (phoneNumber) { this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country); humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(phoneNumber.phone)); } } - this.mgmtService - .addHumanUser(humanReq) - .then((data) => { - this.loading = false; - this.toast.showInfo('USER.TOAST.CREATED', true); - this.router.navigate(['users', data.userId], { queryParams: { new: true } }); - }) - .catch((error) => { - this.loading = false; - this.toast.showError(error); - }); + try { + const data = await this.mgmtService.addHumanUser(humanReq); + this.toast.showInfo('USER.TOAST.CREATED', true); + await this.router.navigate(['users', data.userId], { queryParams: { new: true } }); + } catch (error) { + this.toast.showError(error); + } finally { + this.loading = false; + } } - public setCountryCallingCode(): void { - let value = (this.phone?.value as string) || ''; + protected setCountryCallingCode(): void { + let value = this.userForm.controls.phone.value; this.countryPhoneCodes.forEach((code) => (value = value.replace(`+${code.countryCallingCode}`, ''))); value = value.trim(); - this.phone?.setValue('+' + this.selected?.countryCallingCode + ' ' + value); + + this.userForm.controls.phone.setValue('+' + this.selected?.countryCallingCode + ' ' + value); } - ngOnInit(): void { - this.countryPhoneCodes = this.countryCallingCodesService.getCountryCallingCodes(); - } - - ngOnDestroy(): void { - this.destroyed$.next(); - this.destroyed$.complete(); - } - - public get email(): AbstractControl | null { - return this.userForm.get('email'); - } - public get isVerified(): AbstractControl | null { - return this.userForm.get('isVerified'); - } - public get userName(): AbstractControl | null { - return this.userForm.get('userName'); - } - public get firstName(): AbstractControl | null { - return this.userForm.get('firstName'); - } - public get lastName(): AbstractControl | null { - return this.userForm.get('lastName'); - } - public get nickName(): AbstractControl | null { - return this.userForm.get('nickName'); - } - public get gender(): AbstractControl | null { - return this.userForm.get('gender'); - } - public get preferredLanguage(): AbstractControl | null { - return this.userForm.get('preferredLanguage'); - } - public get phone(): AbstractControl | null { - return this.userForm.get('phone'); - } - - public get password(): AbstractControl | null { - return this.pwdForm.get('password'); - } - public get confirmPassword(): AbstractControl | null { - return this.pwdForm.get('confirmPassword'); - } - - public get envSuffix(): string { - if (this.userLoginMustBeDomain && this.primaryDomain?.domainName) { - return `@${this.primaryDomain.domainName}`; - } else { - return ''; - } - } - - public get suffixPadding(): string | undefined { - if (this.suffix?.nativeElement.offsetWidth) { - return `${(this.suffix.nativeElement as HTMLElement).offsetWidth + 10}px`; - } else { - return; - } - } - - public compareCountries(i1: CountryPhoneCode, i2: CountryPhoneCode) { + protected compareCountries(i1: CountryPhoneCode, i2: CountryPhoneCode) { return ( i1 && i2 && diff --git a/console/src/app/pages/users/user-create/user-create.module.ts b/console/src/app/pages/users/user-create/user-create.module.ts index a5483f1802..5bab56f861 100644 --- a/console/src/app/pages/users/user-create/user-create.module.ts +++ b/console/src/app/pages/users/user-create/user-create.module.ts @@ -19,9 +19,11 @@ import { PasswordComplexityViewModule } from 'src/app/modules/password-complexit import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service'; import { UserCreateRoutingModule } from './user-create-routing.module'; import { UserCreateComponent } from './user-create.component'; +import { UserCreateV2Component } from './user-create-v2/user-create-v2.component'; +import { MatRadioModule } from '@angular/material/radio'; @NgModule({ - declarations: [UserCreateComponent], + declarations: [UserCreateComponent, UserCreateV2Component], providers: [CountryCallingCodesService], imports: [ UserCreateRoutingModule, @@ -42,6 +44,7 @@ import { UserCreateComponent } from './user-create.component'; DetailLayoutModule, InputModule, MatRippleModule, + MatRadioModule, ], }) export default class UserCreateModule {} diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html index 44b8f83548..5f25ee873d 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html @@ -11,12 +11,7 @@ > refresh - + - - - + + + + - - -

{{ 'USER.PAGES.DELETEACCOUNT_DESC' | translate }}

+ + +

{{ 'USER.PAGES.DELETEACCOUNT_DESC' | translate }}

- +
+ +
+ +
+ diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts index 5c79274bb0..b29d21dc9e 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts @@ -1,56 +1,66 @@ import { MediaMatcher } from '@angular/cdk/layout'; -import { Location } from '@angular/common'; -import { Component, EventEmitter, OnDestroy } from '@angular/core'; +import { Component, DestroyRef, EventEmitter, OnInit, signal } from '@angular/core'; import { Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; -import { ActivatedRoute, Params } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Buffer } from 'buffer'; -import { from, Observable, Subscription, take } from 'rxjs'; +import { defer, EMPTY, mergeWith, Observable, of, shareReplay, Subject, switchMap, take } from 'rxjs'; import { ChangeType } from 'src/app/modules/changes/changes.component'; import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; import { InfoDialogComponent } from 'src/app/modules/info-dialog/info-dialog.component'; -import { MetadataDialogComponent } from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component'; +import { + MetadataDialogComponent, + MetadataDialogData, +} from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component'; import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum'; import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; -import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; -import { Email, Gender, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { AuthenticationService } from 'src/app/services/authentication.service'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; -import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; import { formatPhone } from 'src/app/utils/formatPhone'; -import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component'; -import { LanguagesService } from '../../../../services/languages.service'; +import { EditDialogComponent, EditDialogData, EditDialogResult, EditDialogType } from './edit-dialog/edit-dialog.component'; +import { LanguagesService } from 'src/app/services/languages.service'; +import { Gender, HumanProfile, HumanUser, User, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { catchError, filter, map, startWith } from 'rxjs/operators'; +import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith'; +import { NewAuthService } from 'src/app/services/new-auth.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NewMgmtService } from 'src/app/services/new-mgmt.service'; +import { Metadata } from '@zitadel/proto/zitadel/metadata_pb'; +import { UserService } from 'src/app/services/user.service'; +import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb'; +import { query } from '@angular/animations'; + +type UserQuery = { state: 'success'; value: User } | { state: 'error'; error: any } | { state: 'loading'; value?: User }; + +type MetadataQuery = + | { state: 'success'; value: Metadata[] } + | { state: 'loading'; value: Metadata[] } + | { state: 'error'; error: any }; + +type UserWithHumanType = Omit & { type: { case: 'human'; value: HumanUser } }; @Component({ selector: 'cnsl-auth-user-detail', templateUrl: './auth-user-detail.component.html', styleUrls: ['./auth-user-detail.component.scss'], }) -export class AuthUserDetailComponent implements OnDestroy { - public user?: User.AsObject; - public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE]; +export class AuthUserDetailComponent implements OnInit { + protected readonly genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE]; - private subscription: Subscription = new Subscription(); - - public loading: boolean = false; - public loadingMetadata: boolean = false; - - public ChangeType: any = ChangeType; + protected readonly ChangeType = ChangeType; public userLoginMustBeDomain: boolean = false; - public UserState: any = UserState; + protected readonly UserState = UserState; - public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER; - public refreshChanges$: EventEmitter = new EventEmitter(); + protected USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER; + protected readonly refreshChanges$: EventEmitter = new EventEmitter(); + protected readonly refreshMetadata$ = new Subject(); - public metadata: Metadata.AsObject[] = []; - - public settingsList: SidenavSetting[] = [ + protected readonly settingsList: SidenavSetting[] = [ { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' }, { id: 'security', i18nKey: 'USER.SETTINGS.SECURITY' }, { id: 'idp', i18nKey: 'USER.SETTINGS.IDP' }, @@ -62,170 +72,197 @@ export class AuthUserDetailComponent implements OnDestroy { requiredRoles: { [PolicyComponentServiceType.MGMT]: ['user.read'] }, }, ]; - public currentSetting: string | undefined = this.settingsList[0].id; - public loginPolicy?: LoginPolicy.AsObject; - private savedLanguage?: string; + protected readonly user$: Observable; + protected readonly metadata$: Observable; + private readonly savedLanguage$: Observable; + protected readonly currentSetting$ = signal(this.settingsList[0]); + protected readonly loginPolicy$: Observable; + protected readonly userName$: Observable; constructor( - public translate: TranslateService, + private translate: TranslateService, private toast: ToastService, - public userService: GrpcAuthService, + protected grpcAuthService: GrpcAuthService, private dialog: MatDialog, private auth: AuthenticationService, - private mgmt: ManagementService, private breadcrumbService: BreadcrumbService, - private mediaMatcher: MediaMatcher, - private _location: Location, - activatedRoute: ActivatedRoute, public langSvc: LanguagesService, + private readonly route: ActivatedRoute, + private readonly newAuthService: NewAuthService, + private readonly newMgmtService: NewMgmtService, + private readonly userService: UserService, + private readonly destroyRef: DestroyRef, + private readonly router: Router, ) { - activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => { - const { id } = params; - if (id) { - this.cleanupTranslation(); - this.currentSetting = id; - } - }); + this.user$ = this.getUser$().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.userName$ = this.getUserName(this.user$); + this.savedLanguage$ = this.getSavedLanguage$(this.user$); + this.metadata$ = this.getMetadata$().pipe(shareReplay({ refCount: true, bufferSize: 1 })); - const mediaq: string = '(max-width: 500px)'; - const small = this.mediaMatcher.matchMedia(mediaq).matches; - if (small) { - this.changeSelection(small); - } - this.mediaMatcher.matchMedia(mediaq).onchange = (small) => { - this.changeSelection(small.matches); - }; - - this.loading = true; - this.refreshUser(); - - this.userService.getMyLoginPolicy().then((policy) => { - if (policy.policy) { - this.loginPolicy = policy.policy; - } - }); + this.loginPolicy$ = defer(() => this.newMgmtService.getLoginPolicy()).pipe( + catchError(() => EMPTY), + map(({ policy }) => policy), + filter(Boolean), + ); } - private changeSelection(small: boolean): void { - this.cleanupTranslation(); - if (small) { - this.currentSetting = undefined; - } else { - this.currentSetting = this.currentSetting === undefined ? this.settingsList[0].id : this.currentSetting; - } - } - - public navigateBack(): void { - this._location.back(); - } - - refreshUser(): void { - this.refreshChanges$.emit(); - this.userService - .getMyUser() - .then((resp) => { - if (resp.user) { - this.user = resp.user; - - this.loadMetadata(); - - this.breadcrumbService.setBreadcrumb([ - new Breadcrumb({ - type: BreadcrumbType.AUTHUSER, - name: this.user.human?.profile?.displayName, - routerLink: ['/users', 'me'], - }), - ]); + getUserName(user$: Observable) { + return user$.pipe( + map((query) => { + const user = this.user(query); + if (!user) { + return ''; } - this.savedLanguage = resp.user?.human?.profile?.preferredLanguage; - this.loading = false; - }) - .catch((error) => { - this.toast.showError(error); - this.loading = false; - }); + if (user.type.case === 'human') { + return user.type.value.profile?.displayName ?? ''; + } + if (user.type.case === 'machine') { + return user.type.value.name; + } + return ''; + }), + ); } - public ngOnDestroy(): void { - this.cleanupTranslation(); - this.subscription.unsubscribe(); + getSavedLanguage$(user$: Observable) { + return user$.pipe( + switchMap((query) => { + if (query.state !== 'success' || query.value.type.case !== 'human') { + return EMPTY; + } + return query.value.type.value.profile?.preferredLanguage ?? EMPTY; + }), + startWith(this.translate.defaultLang), + ); } - public settingChanged(): void { - this.cleanupTranslation(); - } + ngOnInit(): void { + this.user$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((query) => { + if ((query.state === 'loading' || query.state === 'success') && query.value?.type.case === 'human') { + this.breadcrumbService.setBreadcrumb([ + new Breadcrumb({ + type: BreadcrumbType.AUTHUSER, + name: query.value.type.value.profile?.displayName, + routerLink: ['/users', 'me'], + }), + ]); + } + }); - private cleanupTranslation(): void { - if (this?.savedLanguage) { - this.translate.use(this?.savedLanguage); - } else { - this.translate.use(this.translate.defaultLang); + this.user$.pipe(mergeWith(this.metadata$), takeUntilDestroyed(this.destroyRef)).subscribe((query) => { + if (query.state == 'error') { + this.toast.showError(query.error); + } + }); + + this.savedLanguage$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((savedLanguage) => this.translate.use(savedLanguage)); + + const param = this.route.snapshot.queryParamMap.get('id'); + if (!param) { + return; } + const setting = this.settingsList.find(({ id }) => id === param); + if (!setting) { + return; + } + this.currentSetting$.set(setting); } - public changeUsername(): void { - const dialogRef = this.dialog.open(EditDialogComponent, { - data: { - confirmKey: 'ACTIONS.CHANGE', - cancelKey: 'ACTIONS.CANCEL', - labelKey: 'ACTIONS.NEWVALUE', - titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE', - descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC', - value: this.user?.userName, - }, + private getUser$(): Observable { + return this.refreshChanges$.pipe( + startWith(true), + switchMap(() => this.getMyUser()), + pairwiseStartWith(undefined), + map(([prev, curr]) => { + if (prev?.state === 'success' && curr.state === 'loading') { + return { state: 'loading', value: prev.value } as const; + } + return curr; + }), + ); + } + + private getMyUser(): Observable { + return this.userService.user$.pipe( + map((user) => ({ state: 'success' as const, value: user })), + catchError((error) => of({ state: 'error', error } as const)), + startWith({ state: 'loading' } as const), + ); + } + + getMetadata$(): Observable { + return this.refreshMetadata$.pipe( + startWith(true), + switchMap(() => this.getMetadata()), + pairwiseStartWith(undefined), + map(([prev, curr]) => { + if (prev?.state === 'success' && curr.state === 'loading') { + return { state: 'loading', value: prev.value } as const; + } + return curr; + }), + ); + } + + private getMetadata(): Observable { + return defer(() => this.newAuthService.listMyMetadata()).pipe( + map((metadata) => ({ state: 'success', value: metadata.result }) as const), + startWith({ state: 'loading', value: [] as Metadata[] } as const), + catchError((error) => of({ state: 'error', error } as const)), + ); + } + + public changeUsername(user: User): void { + const data = { + confirmKey: 'ACTIONS.CHANGE' as const, + cancelKey: 'ACTIONS.CANCEL' as const, + labelKey: 'ACTIONS.NEWVALUE' as const, + titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE' as const, + descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC' as const, + value: user.username, + }; + const dialogRef = this.dialog.open(EditDialogComponent, { + data, width: '400px', }); - dialogRef.afterClosed().subscribe((resp: { value: string }) => { - if (resp && resp.value && resp.value !== this.user?.userName) { - this.userService - .updateMyUserName(resp.value) - .then(() => { - this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true); - this.refreshUser(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - }); - } - - public saveProfile(profileData: Profile.AsObject): void { - if (this.user?.human) { - this.user.human.profile = profileData; - - this.userService - .updateMyProfile( - this.user.human.profile?.firstName, - this.user.human.profile?.lastName, - this.user.human.profile?.nickName, - this.user.human.profile?.displayName, - this.user.human.profile?.preferredLanguage, - this.user.human.profile?.gender, - ) - .then(() => { - this.toast.showInfo('USER.TOAST.SAVED', true); - this.savedLanguage = this.user?.human?.profile?.preferredLanguage; + dialogRef + .afterClosed() + .pipe( + map((value) => value?.value), + filter(Boolean), + filter((value) => user.username != value), + switchMap((username) => this.userService.updateUser({ userId: user.userId, username })), + ) + .subscribe({ + next: () => { + this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true); this.refreshChanges$.emit(); - }) - .catch((error) => { + }, + error: (error) => { this.toast.showError(error); - }); - } + }, + }); } - public saveEmail(email: string): void { + public saveProfile(user: User, profile: HumanProfile): void { this.userService - .setMyEmail(email) + .updateUser({ + userId: user.userId, + profile: { + givenName: profile.givenName, + familyName: profile.familyName, + nickName: profile.nickName, + displayName: profile.displayName, + preferredLanguage: profile.preferredLanguage, + gender: profile.gender, + }, + }) .then(() => { - this.toast.showInfo('USER.TOAST.EMAILSAVED', true); - if (this.user?.human) { - const mailToSet = new Email(); - mailToSet.setEmail(email); - this.user.human.email = mailToSet.toObject(); - this.refreshUser(); - } + this.toast.showInfo('USER.TOAST.SAVED', true); + this.refreshChanges$.emit(); }) .catch((error) => { this.toast.showError(error); @@ -233,11 +270,11 @@ export class AuthUserDetailComponent implements OnDestroy { } public enteredPhoneCode(code: string): void { - this.userService + this.newAuthService .verifyMyPhone(code) .then(() => { this.toast.showInfo('USER.TOAST.PHONESAVED', true); - this.refreshUser(); + this.refreshChanges$.emit(); this.promptSetupforSMSOTP(); }) .catch((error) => { @@ -256,39 +293,26 @@ export class AuthUserDetailComponent implements OnDestroy { width: '400px', }); - dialogRef.afterClosed().subscribe((resp) => { - if (resp) { - this.userService.addMyAuthFactorOTPSMS().then(() => { - this.translate - .get('USER.MFA.OTPSMSSUCCESS') - .pipe(take(1)) - .subscribe((msg) => { - this.toast.showInfo(msg); - }); - }); - } - }); + dialogRef + .afterClosed() + .pipe( + filter(Boolean), + switchMap(() => this.newAuthService.addMyAuthFactorOTPSMS()), + switchMap(() => this.translate.get('USER.MFA.OTPSMSSUCCESS').pipe(take(1))), + ) + .subscribe({ + next: (msg) => this.toast.showInfo(msg), + error: (err) => this.toast.showError(err), + }); } public changedLanguage(language: string): void { this.translate.use(language); } - public resendPhoneVerification(): void { - this.userService - .resendMyPhoneVerification() - .then(() => { - this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true); - this.refreshChanges$.emit(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - - public resendEmailVerification(): void { - this.userService - .resendMyEmailVerification() + public resendEmailVerification(user: User): void { + this.newMgmtService + .resendHumanEmailVerification(user.userId) .then(() => { this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true); this.refreshChanges$.emit(); @@ -298,161 +322,187 @@ export class AuthUserDetailComponent implements OnDestroy { }); } - public deletePhone(): void { - this.userService - .removeMyPhone() + public resendPhoneVerification(user: User): void { + this.newMgmtService + .resendHumanPhoneVerification(user.userId) .then(() => { - this.toast.showInfo('USER.TOAST.PHONEREMOVED', true); - if (this.user?.human?.phone) { - const phone = new Phone(); - this.user.human.phone = phone.toObject(); - this.refreshUser(); - } + this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true); + this.refreshChanges$.emit(); }) .catch((error) => { this.toast.showError(error); }); } - public savePhone(phone: string): void { - if (this.user?.human) { - // Format phone before save (add +) - const formattedPhone = formatPhone(phone); - if (formattedPhone) { - phone = formattedPhone.phone; - } - - this.userService - .setMyPhone(phone) - .then(() => { - this.toast.showInfo('USER.TOAST.PHONESAVED', true); - if (this.user?.human) { - const phoneToSet = new Phone(); - phoneToSet.setPhone(phone); - this.user.human.phone = phoneToSet.toObject(); - this.refreshUser(); - } - }) - .catch((error) => { - this.toast.showError(error); - }); - } + public deletePhone(user: User): void { + this.userService + .removePhone(user.userId) + .then(() => { + this.toast.showInfo('USER.TOAST.PHONEREMOVED', true); + this.refreshChanges$.emit(); + }) + .catch((error) => { + this.toast.showError(error); + }); } - public openEditDialog(type: EditDialogType): void { + public openEditDialog(user: UserWithHumanType, type: EditDialogType): void { switch (type) { case EditDialogType.PHONE: - const dialogRefPhone = this.dialog.open(EditDialogComponent, { - data: { - confirmKey: 'ACTIONS.SAVE', - cancelKey: 'ACTIONS.CANCEL', - labelKey: 'USER.LOGINMETHODS.PHONE.EDITVALUE', - titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE', - descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC', - value: this.user?.human?.phone?.phone, - type: type, - validator: Validators.compose([phoneValidator, requiredValidator]), - }, - width: '400px', - }); - - dialogRefPhone.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => { - if (resp && resp.value) { - this.savePhone(resp.value); - } - }); - break; + this.openEditPhoneDialog(user); + return; case EditDialogType.EMAIL: - const dialogRefEmail = this.dialog.open(EditDialogComponent, { - data: { - confirmKey: 'ACTIONS.SAVE', - cancelKey: 'ACTIONS.CANCEL', - labelKey: 'ACTIONS.NEWVALUE', - titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE', - descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC', - value: this.user?.human?.email?.email, - type: type, - }, - width: '400px', - }); - - dialogRefEmail.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => { - if (resp && resp.value) { - this.saveEmail(resp.value); - } - }); - break; + this.openEditEmailDialog(user); + return; } } - public deleteAccount(): void { - const dialogRef = this.dialog.open(WarnDialogComponent, { - data: { - confirmKey: 'USER.DIALOG.DELETE_BTN', - cancelKey: 'ACTIONS.CANCEL', - titleKey: 'USER.DIALOG.DELETE_TITLE', - descriptionKey: 'USER.DIALOG.DELETE_AUTH_DESCRIPTION', - }, + private openEditEmailDialog(user: UserWithHumanType) { + const data: EditDialogData = { + confirmKey: 'ACTIONS.SAVE', + cancelKey: 'ACTIONS.CANCEL', + labelKey: 'ACTIONS.NEWVALUE', + titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE', + descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC', + value: user.type.value?.email?.email, + type: EditDialogType.EMAIL, + } as const; + + const dialogRefEmail = this.dialog.open(EditDialogComponent, { + data, width: '400px', }); - dialogRef.afterClosed().subscribe((resp) => { - if (resp) { - this.userService - .RemoveMyUser() - .then(() => { - this.toast.showInfo('USER.PAGES.DELETEACCOUNT_SUCCESS', true); - this.auth.signout(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } + dialogRefEmail + .afterClosed() + .pipe( + filter((resp): resp is Required => !!resp?.value), + switchMap(({ value, isVerified }) => + this.userService.setEmail({ + userId: user.userId, + email: value, + verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined }, + }), + ), + ) + .subscribe({ + next: () => { + this.toast.showInfo('USER.TOAST.EMAILSAVED', true); + this.refreshChanges$.emit(); + }, + error: (error) => this.toast.showError(error), + }); + } + + private openEditPhoneDialog(user: UserWithHumanType) { + const data = { + confirmKey: 'ACTIONS.SAVE', + cancelKey: 'ACTIONS.CANCEL', + labelKey: 'ACTIONS.NEWVALUE', + titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE', + descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC', + value: user.type.value.phone?.phone, + type: EditDialogType.PHONE, + validator: Validators.compose([phoneValidator, requiredValidator]), + }; + const dialogRefPhone = this.dialog.open( + EditDialogComponent, + { data, width: '400px' }, + ); + + dialogRefPhone + .afterClosed() + .pipe( + map((resp) => formatPhone(resp?.value)), + filter(Boolean), + switchMap(({ phone }) => this.userService.setPhone({ userId: user.userId, phone })), + ) + .subscribe({ + next: () => { + this.toast.showInfo('USER.TOAST.PHONESAVED', true); + this.refreshChanges$.emit(); + }, + error: (error) => { + this.toast.showError(error); + }, + }); + } + + public deleteUser(user: User): void { + const data = { + confirmKey: 'USER.DIALOG.DELETE_BTN', + cancelKey: 'ACTIONS.CANCEL', + titleKey: 'USER.DIALOG.DELETE_TITLE', + descriptionKey: 'USER.DIALOG.DELETE_AUTH_DESCRIPTION', + }; + + const dialogRef = this.dialog.open(WarnDialogComponent, { + width: '400px', + }); + + dialogRef + .afterClosed() + .pipe( + filter(Boolean), + switchMap(() => this.userService.deleteUser(user.userId)), + ) + .subscribe({ + next: () => { + this.toast.showInfo('USER.PAGES.DELETEACCOUNT_SUCCESS', true); + this.auth.signout(); + }, + error: (error) => this.toast.showError(error), + }); + } + + public editMetadata(user: User, metadata: Metadata[]): void { + const setFcn = (key: string, value: string) => + this.newMgmtService.setUserMetadata({ + key, + value: Buffer.from(value), + id: user.userId, + }); + const removeFcn = (key: string): Promise => this.newMgmtService.removeUserMetadata({ key, id: user.userId }); + + const dialogRef = this.dialog.open(MetadataDialogComponent, { + data: { + metadata: [...metadata], + setFcn: setFcn, + removeFcn: removeFcn, + }, + }); + + dialogRef + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.refreshMetadata$.next(true); + }); + } + + protected readonly query = query; + + protected user(user: UserQuery): User | undefined { + if (user.state === 'success' || user.state === 'loading') { + return user.value; + } + return; + } + + public async goToSetting(setting: string) { + await this.router.navigate([], { + relativeTo: this.route, + queryParams: { id: setting }, + queryParamsHandling: 'merge', + skipLocationChange: true, }); } - public loadMetadata(): void { - if (this.user) { - this.userService.isAllowed(['user.read']).subscribe((allowed) => { - if (allowed) { - this.loadingMetadata = true; - this.mgmt - .listUserMetadata(this.user?.id ?? '') - .then((resp) => { - this.loadingMetadata = false; - this.metadata = resp.resultList.map((md) => { - return { - key: md.key, - value: Buffer.from(md.value as string, 'base64').toString('utf8'), - }; - }); - }) - .catch((error) => { - this.loadingMetadata = false; - this.toast.showError(error); - }); - } - }); - } - } - - public editMetadata(): void { - if (this.user && this.user.id) { - const setFcn = (key: string, value: string): Promise => - this.mgmt.setUserMetadata(key, Buffer.from(value).toString('base64'), this.user?.id ?? ''); - const removeFcn = (key: string): Promise => this.mgmt.removeUserMetadata(key, this.user?.id ?? ''); - - const dialogRef = this.dialog.open(MetadataDialogComponent, { - data: { - metadata: this.metadata, - setFcn: setFcn, - removeFcn: removeFcn, - }, - }); - - dialogRef.afterClosed().subscribe(() => { - this.loadMetadata(); - }); + public humanUser(userQuery: UserQuery): UserWithHumanType | undefined { + const user = this.user(userQuery); + if (user?.type.case === 'human') { + return { ...user, type: user.type }; } + return; } } diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html index 04cc701c08..5e4b8065eb 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html @@ -9,12 +9,7 @@ refresh - +
@@ -34,11 +34,11 @@
{{ 'USER.PROFILE.FIRSTNAME' | translate }} - + {{ 'USER.PROFILE.LASTNAME' | translate }} - + {{ 'USER.PROFILE.NICKNAME' | translate }} @@ -67,7 +67,7 @@
-
diff --git a/console/src/app/pages/users/user-detail/detail-form/detail-form.component.ts b/console/src/app/pages/users/user-detail/detail-form/detail-form.component.ts index 052f5b642f..ce72f00271 100644 --- a/console/src/app/pages/users/user-detail/detail-form/detail-form.component.ts +++ b/console/src/app/pages/users/user-detail/detail-form/detail-form.component.ts @@ -1,116 +1,138 @@ -import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core'; -import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; -import { Subscription } from 'rxjs'; +import { combineLatestWith, distinctUntilChanged, ReplaySubject } from 'rxjs'; import { requiredValidator } from 'src/app/modules/form-field/validators/validators'; -import { Gender, Human, Profile } from 'src/app/proto/generated/zitadel/user_pb'; - import { ProfilePictureComponent } from './profile-picture/profile-picture.component'; +import { Gender, HumanProfile, HumanProfileSchema } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { filter, startWith } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Profile } from '@zitadel/proto/zitadel/user_pb'; +import { create } from '@bufbuild/protobuf'; + +function toHumanProfile(profile: HumanProfile | Profile): HumanProfile { + if (profile.$typeName === 'zitadel.user.v2.HumanProfile') { + return profile; + } + return create(HumanProfileSchema, { + givenName: profile.firstName, + familyName: profile.lastName, + nickName: profile.nickName, + displayName: profile.displayName, + preferredLanguage: profile.preferredLanguage, + gender: profile.gender, + avatarUrl: profile.avatarUrl, + }); +} @Component({ selector: 'cnsl-detail-form', templateUrl: './detail-form.component.html', styleUrls: ['./detail-form.component.scss'], }) -export class DetailFormComponent implements OnDestroy, OnChanges { +export class DetailFormComponent implements OnInit { @Input() public showEditImage: boolean = false; @Input() public preferredLoginName: string = ''; - @Input() public username!: string; - @Input() public user!: Human.AsObject; - @Input() public disabled: boolean = true; + @Input({ required: true }) public set username(username: string) { + this.username$.next(username); + } + @Input({ required: true }) public set profile(profile: HumanProfile | Profile) { + this.profile$.next(toHumanProfile(profile)); + } + @Input() public set disabled(disabled: boolean) { + this.disabled$.next(disabled); + } @Input() public genders: Gender[] = []; @Input() public languages: string[] = ['de', 'en']; - @Output() public submitData: EventEmitter = new EventEmitter(); @Output() public changedLanguage: EventEmitter = new EventEmitter(); @Output() public changeUsernameClicked: EventEmitter = new EventEmitter(); @Output() public avatarChanged: EventEmitter = new EventEmitter(); - public profileForm!: UntypedFormGroup; - - private sub: Subscription = new Subscription(); + private username$ = new ReplaySubject(1); + public profile$ = new ReplaySubject(1); + public profileForm!: ReturnType; + public disabled$ = new ReplaySubject(1); + @Output() public submitData = new EventEmitter(); constructor( - private fb: UntypedFormBuilder, - private dialog: MatDialog, + private readonly fb: FormBuilder, + private readonly dialog: MatDialog, + private readonly destroyRef: DestroyRef, ) { - this.profileForm = this.fb.group({ - userName: [{ value: '', disabled: true }, [requiredValidator]], - firstName: [{ value: '', disabled: this.disabled }, requiredValidator], - lastName: [{ value: '', disabled: this.disabled }, requiredValidator], - nickName: [{ value: '', disabled: this.disabled }], - displayName: [{ value: '', disabled: this.disabled }, requiredValidator], - gender: [{ value: 0, disabled: this.disabled }], - preferredLanguage: [{ value: '', disabled: this.disabled }], - }); + this.profileForm = this.buildForm(); } - public ngOnChanges(): void { - this.profileForm = this.fb.group({ - userName: [{ value: '', disabled: true }, [requiredValidator]], - firstName: [{ value: '', disabled: this.disabled }, requiredValidator], - lastName: [{ value: '', disabled: this.disabled }, requiredValidator], - nickName: [{ value: '', disabled: this.disabled }], - displayName: [{ value: '', disabled: this.disabled }, requiredValidator], - gender: [{ value: 0, disabled: this.disabled }], - preferredLanguage: [{ value: '', disabled: this.disabled }], + ngOnInit(): void { + this.profileForm.controls.preferredLanguage.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => this.changedLanguage.emit(value)); + } + + private buildForm() { + const form = this.fb.group({ + username: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + givenName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + familyName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + nickName: new FormControl('', { nonNullable: true }), + displayName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + preferredLanguage: new FormControl('', { nonNullable: true }), + gender: new FormControl(Gender.UNSPECIFIED, { nonNullable: true }), }); - this.profileForm.patchValue({ userName: this.username, ...this.user.profile }); - - if (this.preferredLanguage) { - this.sub = this.preferredLanguage.valueChanges.subscribe((value) => { - this.changedLanguage.emit(value); + form.controls.username.disable(); + this.disabled$ + .pipe(startWith(true), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) + .subscribe((disabled) => { + this.toggleFormControl(form.controls.givenName, disabled); + this.toggleFormControl(form.controls.familyName, disabled); + this.toggleFormControl(form.controls.nickName, disabled); + this.toggleFormControl(form.controls.displayName, disabled); + this.toggleFormControl(form.controls.gender, disabled); + this.toggleFormControl(form.controls.preferredLanguage, disabled); }); - } + + this.username$ + .pipe(combineLatestWith(this.profile$), takeUntilDestroyed(this.destroyRef)) + .subscribe(([username, profile]) => { + form.patchValue({ + username: username, + ...profile, + }); + }); + + return form; } - public ngOnDestroy(): void { - this.sub.unsubscribe(); - } - - public submitForm(): void { - this.submitData.emit(this.profileForm.value); + public submitForm(profile: HumanProfile): void { + this.submitData.emit({ ...profile, ...this.profileForm.getRawValue() }); } public changeUsername(): void { this.changeUsernameClicked.emit(); } - public openUploadDialog(): void { - const dialogRef = this.dialog.open(ProfilePictureComponent, { - data: { - profilePic: this.user.profile?.avatarUrl, - }, + public openUploadDialog(profile: HumanProfile): void { + const data = { + profilePic: profile.avatarUrl, + }; + + const dialogRef = this.dialog.open(ProfilePictureComponent, { width: '400px', }); - dialogRef.afterClosed().subscribe((shouldReload) => { - if (shouldReload) { + dialogRef + .afterClosed() + .pipe(filter(Boolean)) + .subscribe(() => { this.avatarChanged.emit(); - } - }); + }); } - public get userName(): AbstractControl | null { - return this.profileForm.get('userName'); - } - - public get firstName(): AbstractControl | null { - return this.profileForm.get('firstName'); - } - public get lastName(): AbstractControl | null { - return this.profileForm.get('lastName'); - } - public get nickName(): AbstractControl | null { - return this.profileForm.get('nickName'); - } - public get displayName(): AbstractControl | null { - return this.profileForm.get('displayName'); - } - public get gender(): AbstractControl | null { - return this.profileForm.get('gender'); - } - public get preferredLanguage(): AbstractControl | null { - return this.profileForm.get('preferredLanguage'); + public toggleFormControl(control: FormControl, disabled: boolean) { + if (disabled) { + control.disable(); + return; + } + control.enable(); } } diff --git a/console/src/app/pages/users/user-detail/external-idps/external-idps.component.html b/console/src/app/pages/users/user-detail/external-idps/external-idps.component.html index 88d5a52a80..8a5ff759d0 100644 --- a/console/src/app/pages/users/user-detail/external-idps/external-idps.component.html +++ b/console/src/app/pages/users/user-detail/external-idps/external-idps.component.html @@ -8,13 +8,7 @@ > refresh - +
diff --git a/console/src/app/pages/users/user-detail/external-idps/external-idps.component.ts b/console/src/app/pages/users/user-detail/external-idps/external-idps.component.ts index dddeb1627b..26f5b4235d 100644 --- a/console/src/app/pages/users/user-detail/external-idps/external-idps.component.ts +++ b/console/src/app/pages/users/user-detail/external-idps/external-idps.component.ts @@ -3,14 +3,14 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatTableDataSource } from '@angular/material/table'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs'; import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { IDPUserLink } from 'src/app/proto/generated/zitadel/idp_pb'; -import { GrpcAuthService } from '../../../../services/grpc-auth.service'; -import { ManagementService } from '../../../../services/mgmt.service'; -import { ToastService } from '../../../../services/toast.service'; +import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; +import { ManagementService } from 'src/app/services/mgmt.service'; +import { ToastService } from 'src/app/services/toast.service'; @Component({ selector: 'cnsl-external-idps', @@ -18,7 +18,7 @@ import { ToastService } from '../../../../services/toast.service'; styleUrls: ['./external-idps.component.scss'], }) export class ExternalIdpsComponent implements OnInit, OnDestroy { - @Input() service!: GrpcAuthService | ManagementService; + @Input({ required: true }) service!: GrpcAuthService | ManagementService; @Input() userId!: string; @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; public totalResult: number = 0; @@ -41,7 +41,7 @@ export class ExternalIdpsComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { - this.getData(10, 0); + this.getData(10, 0).then(); } ngOnDestroy(): void { @@ -65,39 +65,37 @@ export class ExternalIdpsComponent implements OnInit, OnDestroy { private async getData(limit: number, offset: number): Promise { this.loadingSubject.next(true); - let promise; - if (this.service instanceof ManagementService) { - promise = (this.service as ManagementService).listHumanLinkedIDPs(this.userId, limit, offset); - } else if (this.service instanceof GrpcAuthService) { - promise = (this.service as GrpcAuthService).listMyLinkedIDPs(limit, offset); + const promise = + this.service instanceof ManagementService + ? (this.service as ManagementService).listHumanLinkedIDPs(this.userId, limit, offset) + : (this.service as GrpcAuthService).listMyLinkedIDPs(limit, offset); + + let resp; + try { + resp = await promise; + } catch (error) { + this.toast.showError(error); + this.loadingSubject.next(false); + return; } - if (promise) { - promise - .then((resp) => { - this.dataSource.data = resp.resultList; - if (resp.details?.viewTimestamp) { - this.viewTimestamp = resp.details.viewTimestamp; - } - if (resp.details?.totalResult) { - this.totalResult = resp.details?.totalResult; - } else { - this.totalResult = 0; - } - this.loadingSubject.next(false); - }) - .catch((error: any) => { - this.toast.showError(error); - this.loadingSubject.next(false); - }); + this.dataSource.data = resp.resultList; + if (resp.details?.viewTimestamp) { + this.viewTimestamp = resp.details.viewTimestamp; } + if (resp.details?.totalResult) { + this.totalResult = resp.details?.totalResult; + } else { + this.totalResult = 0; + } + this.loadingSubject.next(false); } public refreshPage(): void { - this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize); + this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize).then(); } - public removeExternalIdp(idp: IDPUserLink.AsObject): void { + public async removeExternalIdp(idp: IDPUserLink.AsObject): Promise { const dialogRef = this.dialog.open(WarnDialogComponent, { data: { confirmKey: 'ACTIONS.REMOVE', @@ -108,27 +106,23 @@ export class ExternalIdpsComponent implements OnInit, OnDestroy { width: '400px', }); - dialogRef.afterClosed().subscribe((resp) => { - if (resp) { - let promise; - if (this.service instanceof ManagementService) { - promise = (this.service as ManagementService).removeHumanLinkedIDP(idp.idpId, idp.providedUserId, idp.userId); - } else if (this.service instanceof GrpcAuthService) { - promise = (this.service as GrpcAuthService).removeMyLinkedIDP(idp.idpId, idp.providedUserId); - } + const resp = await firstValueFrom(dialogRef.afterClosed()); + if (!resp) { + return; + } - if (promise) { - promise - .then((_) => { - setTimeout(() => { - this.refreshPage(); - }, 1000); - }) - .catch((error: any) => { - this.toast.showError(error); - }); - } - } - }); + const promise = + this.service instanceof ManagementService + ? (this.service as ManagementService).removeHumanLinkedIDP(idp.idpId, idp.providedUserId, idp.userId) + : (this.service as GrpcAuthService).removeMyLinkedIDP(idp.idpId, idp.providedUserId); + + try { + await promise; + setTimeout(() => { + this.refreshPage(); + }, 1000); + } catch (error) { + this.toast.showError(error); + } } } diff --git a/console/src/app/pages/users/user-detail/password/password.component.html b/console/src/app/pages/users/user-detail/password/password.component.html index 43cca85fd9..10f3064f0d 100644 --- a/console/src/app/pages/users/user-detail/password/password.component.html +++ b/console/src/app/pages/users/user-detail/password/password.component.html @@ -1,9 +1,9 @@ - +

{{ 'USER.PASSWORD.DESCRIPTION' | translate }}

- - + + -
- +
+ +
-
- -
+ + -
- +
+ +
@@ -74,9 +76,9 @@
- -
+
diff --git a/console/src/app/pages/users/user-detail/password/password.component.ts b/console/src/app/pages/users/user-detail/password/password.component.ts index 904981047a..ef18559e09 100644 --- a/console/src/app/pages/users/user-detail/password/password.component.ts +++ b/console/src/app/pages/users/user-detail/password/password.component.ts @@ -1,174 +1,206 @@ -import { Component, OnDestroy } from '@angular/core'; -import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Component, DestroyRef, OnInit } from '@angular/core'; +import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { Subject, Subscription, take, takeUntil } from 'rxjs'; import { - containsLowerCaseValidator, - containsNumberValidator, - containsSymbolValidator, - containsUpperCaseValidator, - minLengthValidator, - passwordConfirmValidator, - requiredValidator, -} from 'src/app/modules/form-field/validators/validators'; -import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; + map, + switchMap, + firstValueFrom, + mergeWith, + Observable, + defer, + of, + shareReplay, + combineLatestWith, + EMPTY, +} from 'rxjs'; +import { passwordConfirmValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; -import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; -import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; +import { catchError, filter } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { UserService } from 'src/app/services/user.service'; +import { User } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { NewAuthService } from 'src/app/services/new-auth.service'; +import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb'; +import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service'; @Component({ selector: 'cnsl-password', templateUrl: './password.component.html', styleUrls: ['./password.component.scss'], }) -export class PasswordComponent implements OnDestroy { - userId: string = ''; - public username: string = ''; - - public policy!: PasswordComplexityPolicy.AsObject; - public passwordForm!: UntypedFormGroup; - - private formSub: Subscription = new Subscription(); - private destroy$: Subject = new Subject(); +export class PasswordComponent implements OnInit { + private readonly breadcrumb$: Observable; + protected readonly username$: Observable; + protected readonly id$: Observable; + protected readonly form$: Observable; + protected readonly passwordPolicy$: Observable; + protected readonly user$: Observable; constructor( - activatedRoute: ActivatedRoute, - private fb: UntypedFormBuilder, - private authService: GrpcAuthService, - private mgmtUserService: ManagementService, - private toast: ToastService, - private breadcrumbService: BreadcrumbService, + private readonly activatedRoute: ActivatedRoute, + private readonly fb: UntypedFormBuilder, + private readonly userService: UserService, + private readonly newAuthService: NewAuthService, + private readonly toast: ToastService, + private readonly breadcrumbService: BreadcrumbService, + private readonly destroyRef: DestroyRef, + private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService, ) { - activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((data) => { - const { username } = data; - this.username = username; + this.id$ = activatedRoute.paramMap.pipe(map((params) => params.get('id') ?? undefined)); + this.user$ = this.getUser().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.username$ = this.getUsername(this.user$); + this.breadcrumb$ = this.getBreadcrumb$(this.id$, this.user$); + this.passwordPolicy$ = this.getPasswordPolicy$().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.form$ = this.getForm$(this.id$, this.passwordPolicy$); + } + + ngOnInit() { + this.breadcrumb$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((breadcrumbs) => { + this.breadcrumbService.setBreadcrumb(breadcrumbs); }); - activatedRoute.params.pipe(takeUntil(this.destroy$)).subscribe((data) => { - const { id } = data; - if (id) { - this.userId = id; - breadcrumbService.setBreadcrumb([ + } + + private getUser() { + return this.userService.user$.pipe( + catchError((err) => { + this.toast.showError(err); + return EMPTY; + }), + ); + } + + private getUsername(user$: Observable) { + const prefferedLoginName$ = user$.pipe(map((user) => user.preferredLoginName)); + + return this.activatedRoute.queryParamMap.pipe( + map((params) => params.get('username')), + filter(Boolean), + mergeWith(prefferedLoginName$), + ); + } + + private getBreadcrumb$(id$: Observable, user$: Observable): Observable { + return id$.pipe( + switchMap(async (id) => { + if (id) { + return [ + new Breadcrumb({ + type: BreadcrumbType.ORG, + routerLink: ['/org'], + }), + ]; + } + const user = await firstValueFrom(user$); + if (!user) { + return []; + } + return [ new Breadcrumb({ - type: BreadcrumbType.ORG, - routerLink: ['/org'], + type: BreadcrumbType.AUTHUSER, + name: (user.type.case === 'human' && user.type.value.profile?.displayName) || undefined, + routerLink: ['/users', 'me'], }), - ]); - } else { - this.authService.user.pipe(take(1)).subscribe((user) => { - if (user) { - this.username = user.preferredLoginName; - this.breadcrumbService.setBreadcrumb([ - new Breadcrumb({ - type: BreadcrumbType.AUTHUSER, - name: user.human?.profile?.displayName, - routerLink: ['/users', 'me'], - }), - ]); - } - }); - } - - const validators: Validators[] = [requiredValidator]; - this.authService - .getMyPasswordComplexityPolicy() - .then((resp) => { - if (resp.policy) { - this.policy = resp.policy; - } - if (this.policy.minLength) { - validators.push(minLengthValidator(this.policy.minLength)); - } - if (this.policy.hasLowercase) { - validators.push(containsLowerCaseValidator); - } - if (this.policy.hasUppercase) { - validators.push(containsUpperCaseValidator); - } - if (this.policy.hasNumber) { - validators.push(containsNumberValidator); - } - if (this.policy.hasSymbol) { - validators.push(containsSymbolValidator); - } - - this.setupForm(validators); - }) - .catch((error) => { - this.setupForm(validators); - }); - }); + ]; + }), + ); } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - this.formSub.unsubscribe(); + private getPasswordPolicy$(): Observable { + return defer(() => this.newAuthService.getMyPasswordComplexityPolicy()).pipe( + map((resp) => resp.policy), + catchError((err) => { + this.toast.showError(err); + return of(undefined); + }), + ); } - setupForm(validators: Validators[]): void { - if (this.userId) { - this.passwordForm = this.fb.group({ - password: ['', validators], - confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]], + private getForm$( + id$: Observable, + policy$: Observable, + ): Observable { + const validators$ = policy$.pipe(map((policy) => this.passwordComplexityValidatorFactory.buildValidators(policy))); + + return id$.pipe( + combineLatestWith(validators$), + map(([id, validators]) => { + if (id) { + return this.fb.group({ + password: ['', validators], + confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]], + }); + } else { + return this.fb.group({ + currentPassword: ['', requiredValidator], + newPassword: ['', validators], + confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]], + }); + } + }), + ); + } + + public async setInitialPassword(userId: string, form: UntypedFormGroup): Promise { + const password = this.password(form)?.value; + + if (form.invalid || !password) { + return; + } + + try { + await this.userService.setPassword({ + userId, + newPassword: { + password, + changeRequired: false, + }, }); - } else { - this.passwordForm = this.fb.group({ - currentPassword: ['', requiredValidator], - newPassword: ['', validators], - confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]], + } catch (error) { + this.toast.showError(error); + return; + } + this.toast.showInfo('USER.TOAST.INITIALPASSWORDSET', true); + window.history.back(); + } + + public async setPassword(form: UntypedFormGroup, user: User): Promise { + const currentPassword = this.currentPassword(form); + const newPassword = this.newPassword(form); + + if (form.invalid || !currentPassword?.value || !newPassword?.value || newPassword?.invalid) { + return; + } + + try { + await this.userService.setPassword({ + userId: user.userId, + newPassword: { + password: newPassword.value, + changeRequired: false, + }, + verification: { + case: 'currentPassword', + value: currentPassword.value, + }, }); + } catch (error) { + this.toast.showError(error); + return; } + this.toast.showInfo('USER.TOAST.PASSWORDCHANGED', true); + window.history.back(); } - public setInitialPassword(userId: string): void { - if (this.passwordForm.valid && this.password && this.password.value) { - this.mgmtUserService - .setHumanInitialPassword(userId, this.password.value) - .then((data: any) => { - this.toast.showInfo('USER.TOAST.INITIALPASSWORDSET', true); - window.history.back(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } + public password(form: UntypedFormGroup): AbstractControl | null { + return form.get('password'); } - public setPassword(): void { - if ( - this.passwordForm.valid && - this.currentPassword && - this.currentPassword.value && - this.newPassword && - this.newPassword.value && - this.newPassword.valid - ) { - this.authService - .updateMyPassword(this.currentPassword.value, this.newPassword.value) - .then((data: any) => { - this.toast.showInfo('USER.TOAST.PASSWORDCHANGED', true); - window.history.back(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } + public newPassword(form: UntypedFormGroup): AbstractControl | null { + return form.get('newPassword'); } - public get password(): AbstractControl | null { - return this.passwordForm.get('password'); - } - - public get newPassword(): AbstractControl | null { - return this.passwordForm.get('newPassword'); - } - - public get currentPassword(): AbstractControl | null { - return this.passwordForm.get('currentPassword'); - } - - public get confirmPassword(): AbstractControl | null { - return this.passwordForm.get('confirmPassword'); + public currentPassword(form: UntypedFormGroup): AbstractControl | null { + return form.get('currentPassword'); } } diff --git a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html index 6726f98f98..d0a3071696 100644 --- a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html @@ -11,10 +11,10 @@ > refresh - +
@@ -42,8 +42,8 @@ {{ 'USER.PASSWORDLESS.STATE.' + mfa.state | translate }} diff --git a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.ts b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.ts index f427bc2dac..d2285fa69c 100644 --- a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.ts @@ -1,12 +1,13 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; -import { MatTable, MatTableDataSource } from '@angular/material/table'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { MatTableDataSource } from '@angular/material/table'; +import { BehaviorSubject, Observable, switchMap } from 'rxjs'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; -import { AuthFactorState, User, WebAuthNToken } from 'src/app/proto/generated/zitadel/user_pb'; -import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; +import { AuthFactorState, Passkey, User } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { UserService } from 'src/app/services/user.service'; +import { filter } from 'rxjs/operators'; export interface WebAuthNOptions { challenge: string; @@ -24,23 +25,22 @@ export interface WebAuthNOptions { styleUrls: ['./passwordless.component.scss'], }) export class PasswordlessComponent implements OnInit, OnDestroy { - @Input() public user!: User.AsObject; + @Input({ required: true }) public user!: User; @Input() public disabled: boolean = true; public displayedColumns: string[] = ['name', 'state', 'actions']; private loadingSubject: BehaviorSubject = new BehaviorSubject(false); public loading$: Observable = this.loadingSubject.asObservable(); - @ViewChild(MatTable) public table!: MatTable; @ViewChild(MatSort) public sort!: MatSort; - public dataSource: MatTableDataSource = new MatTableDataSource([]); + public dataSource: MatTableDataSource = new MatTableDataSource([]); - public AuthFactorState: any = AuthFactorState; + public AuthFactorState = AuthFactorState; public error: string = ''; constructor( - private service: ManagementService, private toast: ToastService, private dialog: MatDialog, + private userService: UserService, ) {} public ngOnInit(): void { @@ -52,10 +52,10 @@ export class PasswordlessComponent implements OnInit, OnDestroy { } public getPasswordless(): void { - this.service - .listHumanPasswordless(this.user.id) + this.userService + .listPasskeys({ userId: this.user.userId }) .then((passwordless) => { - this.dataSource = new MatTableDataSource(passwordless.resultList); + this.dataSource = new MatTableDataSource(passwordless.result); this.dataSource.sort = this.sort; }) .catch((error) => { @@ -63,7 +63,7 @@ export class PasswordlessComponent implements OnInit, OnDestroy { }); } - public deletePasswordless(id?: string): void { + public deletePasswordless(passkeyId: string): void { const dialogRef = this.dialog.open(WarnDialogComponent, { data: { confirmKey: 'ACTIONS.DELETE', @@ -74,24 +74,26 @@ export class PasswordlessComponent implements OnInit, OnDestroy { width: '400px', }); - dialogRef.afterClosed().subscribe((resp) => { - if (resp && id) { - this.service - .removeHumanPasswordless(id, this.user.id) - .then(() => { - this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true); - this.getPasswordless(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - }); + dialogRef + .afterClosed() + .pipe( + filter(Boolean), + switchMap(() => this.userService.removePasskeys({ userId: this.user.userId, passkeyId })), + ) + .subscribe({ + next: () => { + this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true); + this.getPasswordless(); + }, + error: (error) => { + this.toast.showError(error); + }, + }); } public sendPasswordlessRegistration(): void { - this.service - .sendPasswordlessRegistration(this.user.id) + this.userService + .createPasskeyRegistrationLink({ userId: this.user.userId, medium: { case: 'sendLink', value: {} } }) .then(() => { this.toast.showInfo('USER.TOAST.PASSWORDLESSREGISTRATIONSENT', true); }) diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html index 9e319b7bc6..3a5bb3161b 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html @@ -1,262 +1,276 @@ - - - - - - - - - + + + + + + + + - - - + + -
-
- -
-
- -
-

{{ 'USER.PAGES.NOUSER' | translate }}

-
- -
- - -
-

{{ error }}

-
- -
- - {{ 'USER.PAGES.LOCKEDDESCRIPTION' | translate }} - {{ 'USER.PAGES.NOUSER' | translate }} - -
- -
- {{ 'USER.ISINITIAL' | translate }} - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- {{ 'USER.PASSWORD.LABEL' | translate }} - ********* - - -
- -
- - - -
-
-
-
- - - - - -
- - - - - - - - - - - - - - - - - -
-
-
- - +
+
+
- -
+
+ +
+

{{ 'USER.PAGES.NOUSER' | translate }}

+
+ + +
+ + +
+ + {{ 'USER.PAGES.LOCKEDDESCRIPTION' | translate }} +
+ +
+ {{ 'USER.ISINITIAL' | translate }} + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ {{ 'USER.PASSWORD.LABEL' | translate }} + ********* + + +
+ +
+ + + +
+
+
+
+ + + + + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + +
+
+
+
+ diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts index 2418baafc4..9370471ea4 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts @@ -1,32 +1,60 @@ -import { MediaMatcher } from '@angular/cdk/layout'; import { Location } from '@angular/common'; -import { Component, EventEmitter, OnInit } from '@angular/core'; +import { Component, DestroyRef, EventEmitter, OnInit, signal } from '@angular/core'; import { Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Buffer } from 'buffer'; -import { take } from 'rxjs/operators'; +import { catchError, filter, map, startWith, take } from 'rxjs/operators'; import { ChangeType } from 'src/app/modules/changes/changes.component'; import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; import { InfoSectionType } from 'src/app/modules/info-section/info-section.component'; -import { MetadataDialogComponent } from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component'; +import { + MetadataDialogComponent, + MetadataDialogData, +} from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component'; import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; -import { SendHumanResetPasswordNotificationRequest, UnlockUserRequest } from 'src/app/proto/generated/zitadel/management_pb'; -import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; -import { Email, Gender, Machine, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; -import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; import { formatPhone } from 'src/app/utils/formatPhone'; -import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component'; -import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component'; +import { + EditDialogData, + EditDialogResult, + EditDialogComponent, + EditDialogType, +} from '../auth-user-detail/edit-dialog/edit-dialog.component'; +import { + ResendEmailDialogComponent, + ResendEmailDialogData, + ResendEmailDialogResult, +} from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component'; import { MachineSecretDialogComponent } from './machine-secret-dialog/machine-secret-dialog.component'; -import { Observable } from 'rxjs'; -import { LanguagesService } from '../../../../services/languages.service'; +import { LanguagesService } from 'src/app/services/languages.service'; +import { UserService } from 'src/app/services/user.service'; +import { Gender, HumanProfile, HumanUser, User as UserV2, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { + combineLatestWith, + defer, + EMPTY, + identity, + mergeWith, + Observable, + ObservedValueOf, + of, + shareReplay, + Subject, + switchMap, +} from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DetailFormMachineComponent } from '../detail-form-machine/detail-form-machine.component'; +import { NewMgmtService } from 'src/app/services/new-mgmt.service'; +import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb'; +import { SendHumanResetPasswordNotificationRequest_Type } from '@zitadel/proto/zitadel/management_pb'; +import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith'; +import { Metadata } from '@zitadel/proto/zitadel/metadata_pb'; +import { ManagementService } from 'src/app/services/mgmt.service'; const GENERAL: SidenavSetting = { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' }; const GRANTS: SidenavSetting = { id: 'grants', i18nKey: 'USER.SETTINGS.USERGRANTS' }; @@ -37,22 +65,32 @@ const PERSONALACCESSTOKEN: SidenavSetting = { id: 'pat', i18nKey: 'USER.SETTINGS const KEYS: SidenavSetting = { id: 'keys', i18nKey: 'USER.SETTINGS.KEYS' }; const MEMBERSHIPS: SidenavSetting = { id: 'memberships', i18nKey: 'USER.SETTINGS.MEMBERSHIPS' }; +type UserQuery = + | { state: 'success'; value: UserV2 } + | { state: 'error'; value: string } + | { state: 'loading'; value?: UserV2 } + | { state: 'notfound' }; + +type MetadataQuery = + | { state: 'success'; value: Metadata[] } + | { state: 'loading'; value: Metadata[] } + | { state: 'error'; value: string }; + +type UserWithHumanType = Omit & { type: { case: 'human'; value: HumanUser } }; + +// todo: figure out why media matcher is needed @Component({ selector: 'cnsl-user-detail', templateUrl: './user-detail.component.html', styleUrls: ['./user-detail.component.scss'], }) export class UserDetailComponent implements OnInit { - public user!: User.AsObject; - public metadata: Metadata.AsObject[] = []; - public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE]; + public user$: Observable; + public genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE]; public ChangeType: any = ChangeType; - public loading: boolean = true; - public loadingMetadata: boolean = true; - - public UserState: any = UserState; + public UserState = UserState; public copied: string = ''; public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER; @@ -60,32 +98,26 @@ export class UserDetailComponent implements OnInit { public refreshChanges$: EventEmitter = new EventEmitter(); public InfoSectionType: any = InfoSectionType; - public error: string = ''; - - public settingsList: SidenavSetting[] = [GENERAL, GRANTS, MEMBERSHIPS, METADATA]; - public currentSetting: string | undefined = 'general'; - public loginPolicy?: LoginPolicy.AsObject; + public currentSetting$ = signal(GENERAL); + public settingsList$: Observable; + public metadata$: Observable; + public loginPolicy$: Observable; + public refreshMetadata$ = new Subject(); constructor( public translate: TranslateService, - private route: ActivatedRoute, + private readonly route: ActivatedRoute, private toast: ToastService, - public mgmtUserService: ManagementService, private _location: Location, private dialog: MatDialog, private router: Router, - activatedRoute: ActivatedRoute, - private mediaMatcher: MediaMatcher, public langSvc: LanguagesService, + private readonly userService: UserService, + private readonly newMgmtService: NewMgmtService, + public readonly mgmtService: ManagementService, breadcrumbService: BreadcrumbService, + private readonly destroyRef: DestroyRef, ) { - activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => { - const { id } = params; - if (id) { - this.currentSetting = id; - } - }); - breadcrumbService.setBreadcrumb([ new Breadcrumb({ type: BreadcrumbType.ORG, @@ -93,63 +125,115 @@ export class UserDetailComponent implements OnInit { }), ]); - const mediaq: string = '(max-width: 500px)'; - const small = this.mediaMatcher.matchMedia(mediaq).matches; - if (small) { - this.changeSelection(small); - } - this.mediaMatcher.matchMedia(mediaq).onchange = (small) => { - this.changeSelection(small.matches); - }; + this.user$ = this.getUser$().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.settingsList$ = this.getSettingsList$(this.user$).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.metadata$ = this.getMetadata$(this.user$).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + + this.loginPolicy$ = defer(() => this.newMgmtService.getLoginPolicy()).pipe( + catchError(() => EMPTY), + map(({ policy }) => policy), + filter(Boolean), + ); } - private changeSelection(small: boolean): void { - if (small) { - this.currentSetting = undefined; - } else { - this.currentSetting = this.currentSetting === undefined ? 'general' : this.currentSetting; - } + private getId$(): Observable { + return this.route.paramMap.pipe( + map((params) => params.get('id')), + filter(Boolean), + ); } - refreshUser(): void { - this.refreshChanges$.emit(); - this.route.params.pipe(take(1)).subscribe((params) => { - this.loading = true; - const { id } = params; - this.mgmtUserService - .getUserByID(id) - .then((resp) => { - this.loadMetadata(id); - this.loading = false; - if (resp.user) { - this.user = resp.user; + private getUser$(): Observable { + return this.getId$().pipe( + combineLatestWith(this.refreshChanges$.pipe(startWith(undefined))), + switchMap(([id]) => this.getUserById(id)), + pairwiseStartWith(undefined), + map(([prev, curr]) => { + if (prev?.state === 'success' && curr.state === 'loading') { + return { state: 'loading', value: prev.value } as const; + } + return curr; + }), + ); + } - if (this.user.human) { - this.settingsList = [GENERAL, SECURITY, IDP, GRANTS, MEMBERSHIPS, METADATA]; - } else if (this.user.machine) { - this.settingsList = [GENERAL, GRANTS, MEMBERSHIPS, PERSONALACCESSTOKEN, KEYS, METADATA]; - } - } - }) - .catch((err) => { - this.error = err.message ?? ''; - this.loading = false; - this.toast.showError(err); - }); - }); + private getSettingsList$(user$: Observable): Observable { + return user$.pipe( + switchMap((user) => { + if (user.state !== 'success') { + return EMPTY; + } + + if (user.value.type.case === 'human') { + return of([GENERAL, SECURITY, IDP, GRANTS, MEMBERSHIPS, METADATA]); + } else if (user.value.type.case === 'machine') { + return of([GENERAL, GRANTS, MEMBERSHIPS, PERSONALACCESSTOKEN, KEYS, METADATA]); + } + return EMPTY; + }), + startWith([GENERAL, GRANTS, MEMBERSHIPS, METADATA]), + ); + } + + private getUserById(userId: string): Observable { + return defer(() => this.userService.getUserById(userId)).pipe( + map(({ user }) => { + if (user) { + return { state: 'success', value: user } as const; + } + return { state: 'notfound' } as const; + }), + catchError((error) => of({ state: 'error', value: error.message ?? '' } as const)), + startWith({ state: 'loading' } as const), + ); + } + + getMetadata$(user$: Observable): Observable { + return this.refreshMetadata$.pipe( + startWith(true), + combineLatestWith(user$), + switchMap(([_, user]) => { + if (!(user.state === 'success' || user.state === 'loading')) { + return EMPTY; + } + if (!user.value) { + return EMPTY; + } + return this.getMetadataById(user.value.userId); + }), + pairwiseStartWith(undefined), + map(([prev, curr]) => { + if (prev?.state === 'success' && curr.state === 'loading') { + return { state: 'loading', value: prev.value } as const; + } + return curr; + }), + ); } public ngOnInit(): void { - this.refreshUser(); - - this.mgmtUserService.getLoginPolicy().then((policy) => { - if (policy.policy) { - this.loginPolicy = policy.policy; + this.user$.pipe(mergeWith(this.metadata$), takeUntilDestroyed(this.destroyRef)).subscribe((query) => { + if (query.state == 'error') { + this.toast.showError(query.value); } }); + + const param = this.route.snapshot.queryParamMap.get('id'); + if (!param) { + return; + } + + this.settingsList$ + .pipe( + takeUntilDestroyed(this.destroyRef), + map((settings) => settings.find(({ id }) => id === param)), + filter(Boolean), + take(1), + ) + .subscribe((setting) => this.currentSetting$.set(setting)); } - public changeUsername(): void { + public changeUsername(user: UserV2): void { const dialogRef = this.dialog.open(EditDialogComponent, { data: { confirmKey: 'ACTIONS.CHANGE', @@ -157,43 +241,45 @@ export class UserDetailComponent implements OnInit { labelKey: 'ACTIONS.NEWVALUE', titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE', descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC', - value: this.user.userName, + value: user.username, }, width: '400px', }); - dialogRef.afterClosed().subscribe((resp: { value: string }) => { - if (resp.value && resp.value !== this.user.userName) { - this.mgmtUserService - .updateUserName(this.user.id, resp.value) - .then(() => { - this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true); - this.refreshUser(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - }); + dialogRef + .afterClosed() + .pipe( + map(({ value }: { value?: string }) => value), + filter(Boolean), + filter((value) => user.username != value), + switchMap((username) => this.userService.updateUser({ userId: user.userId, username })), + ) + .subscribe({ + next: () => { + this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true); + this.refreshChanges$.emit(); + }, + error: (error) => { + this.toast.showError(error); + }, + }); } - public unlockUser(): void { - const req = new UnlockUserRequest(); - req.setId(this.user.id); - this.mgmtUserService - .unlockUser(req) + public unlockUser(user: UserV2): void { + this.userService + .unlockUser(user.userId) .then(() => { this.toast.showInfo('USER.TOAST.UNLOCKED', true); - this.refreshUser(); + this.refreshChanges$.emit(); }) .catch((error) => { this.toast.showError(error); }); } - public generateMachineSecret(): void { - this.mgmtUserService - .generateMachineSecret(this.user.id) + public generateMachineSecret(user: UserV2): void { + this.newMgmtService + .generateMachineSecret(user.userId) .then((resp) => { this.toast.showInfo('USER.TOAST.SECRETGENERATED', true); this.dialog.open(MachineSecretDialogComponent, { @@ -203,64 +289,41 @@ export class UserDetailComponent implements OnInit { }, width: '400px', }); - this.refreshUser(); + this.refreshChanges$.emit(); }) .catch((error) => { this.toast.showError(error); }); } - public removeMachineSecret(): void { - this.mgmtUserService - .removeMachineSecret(this.user.id) - .then((resp) => { + public removeMachineSecret(user: UserV2): void { + this.newMgmtService + .removeMachineSecret(user.userId) + .then(() => { this.toast.showInfo('USER.TOAST.SECRETREMOVED', true); - this.refreshUser(); + this.refreshChanges$.emit(); }) .catch((error) => { this.toast.showError(error); }); } - public changeState(newState: UserState): void { - if (newState === UserState.USER_STATE_ACTIVE) { - this.mgmtUserService - .reactivateUser(this.user.id) + public changeState(user: UserV2, newState: UserState): void { + if (newState === UserState.ACTIVE) { + this.userService + .reactivateUser(user.userId) .then(() => { this.toast.showInfo('USER.TOAST.REACTIVATED', true); - this.user.state = newState; + this.refreshChanges$.emit(); }) .catch((error) => { this.toast.showError(error); }); - } else if (newState === UserState.USER_STATE_INACTIVE) { - this.mgmtUserService - .deactivateUser(this.user.id) + } else if (newState === UserState.INACTIVE) { + this.userService + .deactivateUser(user.userId) .then(() => { this.toast.showInfo('USER.TOAST.DEACTIVATED', true); - this.user.state = newState; - }) - .catch((error) => { - this.toast.showError(error); - }); - } - } - - public saveProfile(profileData: Profile.AsObject): void { - if (this.user.human) { - this.user.human.profile = profileData; - this.mgmtUserService - .updateHumanProfile( - this.user.id, - this.user.human.profile.firstName, - this.user.human.profile.lastName, - this.user.human.profile.nickName, - this.user.human.profile.displayName, - this.user.human.profile.preferredLanguage, - this.user.human.profile.gender, - ) - .then(() => { - this.toast.showInfo('USER.TOAST.SAVED', true); this.refreshChanges$.emit(); }) .catch((error) => { @@ -269,32 +332,48 @@ export class UserDetailComponent implements OnInit { } } - public saveMachine(machineData: Machine.AsObject): void { - if (this.user.machine) { - this.user.machine.name = machineData.name; - this.user.machine.description = machineData.description; - this.user.machine.accessTokenType = machineData.accessTokenType; - - this.mgmtUserService - .updateMachine( - this.user.id, - this.user.machine.name, - this.user.machine.description, - this.user.machine.accessTokenType, - ) - .then(() => { - this.toast.showInfo('USER.TOAST.SAVED', true); - this.refreshChanges$.emit(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } + public saveProfile(user: UserV2, profile: HumanProfile): void { + this.userService + .updateUser({ + userId: user.userId, + profile: { + givenName: profile.givenName, + familyName: profile.familyName, + nickName: profile.nickName, + displayName: profile.displayName, + preferredLanguage: profile.preferredLanguage, + gender: profile.gender, + }, + }) + .then(() => { + this.toast.showInfo('USER.TOAST.SAVED', true); + this.refreshChanges$.emit(); + }) + .catch((error) => { + this.toast.showError(error); + }); } - public resendEmailVerification(): void { - this.mgmtUserService - .resendHumanEmailVerification(this.user.id) + public saveMachine(user: UserV2, form: ObservedValueOf): void { + this.newMgmtService + .updateMachine({ + userId: user.userId, + name: form.name, + description: form.description, + accessTokenType: form.accessTokenType, + }) + .then(() => { + this.toast.showInfo('USER.TOAST.SAVED', true); + this.refreshChanges$.emit(); + }) + .catch((error) => { + this.toast.showError(error); + }); + } + + public resendEmailVerification(user: UserV2): void { + this.newMgmtService + .resendHumanEmailVerification(user.userId) .then(() => { this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true); this.refreshChanges$.emit(); @@ -304,9 +383,9 @@ export class UserDetailComponent implements OnInit { }); } - public resendPhoneVerification(): void { - this.mgmtUserService - .resendHumanPhoneVerification(this.user.id) + public resendPhoneVerification(user: UserV2): void { + this.newMgmtService + .resendHumanPhoneVerification(user.userId) .then(() => { this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true); this.refreshChanges$.emit(); @@ -316,79 +395,25 @@ export class UserDetailComponent implements OnInit { }); } - public deletePhone(): void { - this.mgmtUserService - .removeHumanPhone(this.user.id) + public deletePhone(user: UserV2): void { + this.userService + .removePhone(user.userId) .then(() => { this.toast.showInfo('USER.TOAST.PHONEREMOVED', true); - if (this.user.human) { - this.user.human.phone = new Phone().setPhone('').toObject(); - this.refreshUser(); - } + this.refreshChanges$.emit(); }) .catch((error) => { this.toast.showError(error); }); } - public saveEmail(email: string, isVerified: boolean): void { - if (this.user.id && email) { - this.mgmtUserService - .updateHumanEmail(this.user.id, email, isVerified) - .then(() => { - this.toast.showInfo('USER.TOAST.EMAILSAVED', true); - if (this.user.state === UserState.USER_STATE_INITIAL) { - this.mgmtUserService - .resendHumanInitialization(this.user.id, email ?? '') - .then(() => { - this.toast.showInfo('USER.TOAST.INITEMAILSENT', true); - this.refreshChanges$.emit(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - if (this.user.human) { - this.user.human.email = new Email().setEmail(email).toObject(); - this.refreshUser(); - } - }) - .catch((error) => { - this.toast.showError(error); - }); - } - } - - public savePhone(phone: string): void { - if (this.user.id && phone) { - // Format phone before save (add +) - const formattedPhone = formatPhone(phone); - if (formattedPhone) { - phone = formattedPhone.phone; - } - - this.mgmtUserService - .updateHumanPhone(this.user.id, phone) - .then(() => { - this.toast.showInfo('USER.TOAST.PHONESAVED', true); - if (this.user.human) { - this.user.human.phone = new Phone().setPhone(phone).toObject(); - this.refreshUser(); - } - }) - .catch((error) => { - this.toast.showError(error); - }); - } - } - public navigateBack(): void { this._location.back(); } - public sendSetPasswordNotification(): void { - this.mgmtUserService - .sendHumanResetPasswordNotification(this.user.id, SendHumanResetPasswordNotificationRequest.Type.TYPE_EMAIL) + public sendSetPasswordNotification(user: UserV2): void { + this.newMgmtService + .sendHumanResetPasswordNotification(user.userId, SendHumanResetPasswordNotificationRequest_Type.EMAIL) .then(() => { this.toast.showInfo('USER.TOAST.PASSWORDNOTIFICATIONSENT', true); this.refreshChanges$.emit(); @@ -398,143 +423,190 @@ export class UserDetailComponent implements OnInit { }); } - public deleteUser(): void { - const dialogRef = this.dialog.open(WarnDialogComponent, { - data: { - confirmKey: 'ACTIONS.DELETE', - cancelKey: 'ACTIONS.CANCEL', - titleKey: 'USER.DIALOG.DELETE_TITLE', - descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION', - }, + public deleteUser(user: UserV2): void { + const data = { + confirmKey: 'ACTIONS.DELETE', + cancelKey: 'ACTIONS.CANCEL', + titleKey: 'USER.DIALOG.DELETE_TITLE', + descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION', + }; + + const dialogRef = this.dialog.open(WarnDialogComponent, { + data, width: '400px', }); - dialogRef.afterClosed().subscribe((resp) => { - if (resp) { - this.mgmtUserService - .removeUser(this.user.id) - .then(() => { - const params: Params = { - deferredReload: true, - type: this.user.human ? 'humans' : 'machines', - }; - this.router.navigate(['/users'], { queryParams: params }); - this.toast.showInfo('USER.TOAST.DELETED', true); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - }); + dialogRef + .afterClosed() + .pipe( + filter(Boolean), + switchMap(() => this.userService.deleteUser(user.userId)), + ) + .subscribe({ + next: () => { + const params: Params = { + deferredReload: true, + type: user.type.case === 'human' ? 'humans' : 'machines', + }; + this.router.navigate(['/users'], { queryParams: params }).then(); + this.toast.showInfo('USER.TOAST.DELETED', true); + }, + error: (error) => this.toast.showError(error), + }); } - public resendInitEmail(): void { - const dialogRef = this.dialog.open(ResendEmailDialogComponent, { - width: '400px', - data: { - email: this.user.human?.email?.email ?? '', + public resendInitEmail(user: UserV2): void { + const dialogRef = this.dialog.open( + ResendEmailDialogComponent, + { + width: '400px', + data: { + email: user.type.case === 'human' ? (user.type.value.email?.email ?? '') : '', + }, }, - }); + ); - dialogRef.afterClosed().subscribe((resp) => { - if (resp.send && this.user.id) { - this.mgmtUserService - .resendHumanInitialization(this.user.id, resp.email ?? '') - .then(() => { - this.toast.showInfo('USER.TOAST.INITEMAILSENT', true); - this.refreshChanges$.emit(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - }); + dialogRef + .afterClosed() + .pipe( + filter((resp): resp is { send: true; email: string } => !!resp?.send && !!user.userId), + switchMap(({ email }) => this.newMgmtService.resendHumanInitialization(user.userId, email)), + ) + .subscribe({ + next: () => { + this.toast.showInfo('USER.TOAST.INITEMAILSENT', true); + this.refreshChanges$.emit(); + }, + error: (error) => this.toast.showError(error), + }); } - public openEditDialog(type: EditDialogType): void { + public openEditDialog(user: UserWithHumanType, type: EditDialogType): void { switch (type) { case EditDialogType.PHONE: - const dialogRefPhone = this.dialog.open(EditDialogComponent, { - data: { - confirmKey: 'ACTIONS.SAVE', - cancelKey: 'ACTIONS.CANCEL', - labelKey: 'ACTIONS.NEWVALUE', - titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE', - descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC', - value: this.user.human?.phone?.phone, - type: EditDialogType.PHONE, - validator: Validators.compose([phoneValidator, requiredValidator]), - }, - width: '400px', - }); - - dialogRefPhone.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => { - if (resp && resp.value) { - this.savePhone(resp.value); - } - }); - break; + this.openEditPhoneDialog(user); + return; case EditDialogType.EMAIL: - const dialogRefEmail = this.dialog.open(EditDialogComponent, { - data: { - confirmKey: 'ACTIONS.SAVE', - cancelKey: 'ACTIONS.CANCEL', - labelKey: 'ACTIONS.NEWVALUE', - titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE', - descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC', - isVerifiedTextKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIED', - isVerifiedTextDescKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC', - value: this.user.human?.email?.email, - type: EditDialogType.EMAIL, - }, - width: '400px', - }); - - dialogRefEmail.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => { - if (resp && resp.value) { - this.saveEmail(resp.value, resp.isVerified); - } - }); - break; + this.openEditEmailDialog(user); + return; } } - public loadMetadata(id: string): Promise | void { - this.loadingMetadata = true; - return this.mgmtUserService - .listUserMetadata(id) - .then((resp) => { - this.loadingMetadata = false; - this.metadata = resp.resultList.map((md) => { - return { - key: md.key, - value: Buffer.from(md.value as string, 'base64').toString('utf-8'), - }; - }); - }) - .catch((error) => { - this.loadingMetadata = false; - this.toast.showError(error); + private openEditEmailDialog(user: UserWithHumanType) { + const data: EditDialogData = { + confirmKey: 'ACTIONS.SAVE', + cancelKey: 'ACTIONS.CANCEL', + labelKey: 'ACTIONS.NEWVALUE', + titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE', + descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC', + isVerifiedTextKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIED', + isVerifiedTextDescKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC', + value: user.type.value?.email?.email, + type: EditDialogType.EMAIL, + } as const; + + const dialogRefEmail = this.dialog.open(EditDialogComponent, { + data, + width: '400px', + }); + + dialogRefEmail + .afterClosed() + .pipe( + filter((resp): resp is Required => !!resp?.value), + switchMap(({ value, isVerified }) => + this.userService.setEmail({ + userId: user.userId, + email: value, + verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined }, + }), + ), + switchMap(() => { + this.toast.showInfo('USER.TOAST.EMAILSAVED', true); + this.refreshChanges$.emit(); + if (user.state !== UserState.INITIAL) { + return EMPTY; + } + return this.userService.resendInviteCode(user.userId); + }), + ) + .subscribe({ + next: () => this.toast.showInfo('USER.TOAST.INITEMAILSENT', true), + error: (error) => this.toast.showError(error), }); } - public editMetadata(): void { - if (this.user) { - const setFcn = (key: string, value: string): Promise => - this.mgmtUserService.setUserMetadata(key, Buffer.from(value).toString('base64'), this.user.id); - const removeFcn = (key: string): Promise => this.mgmtUserService.removeUserMetadata(key, this.user.id); + private openEditPhoneDialog(user: UserWithHumanType) { + const data = { + confirmKey: 'ACTIONS.SAVE', + cancelKey: 'ACTIONS.CANCEL', + labelKey: 'ACTIONS.NEWVALUE', + titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE', + descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC', + value: user.type.value.phone?.phone, + type: EditDialogType.PHONE, + validator: Validators.compose([phoneValidator, requiredValidator]), + }; + const dialogRefPhone = this.dialog.open( + EditDialogComponent, + { data, width: '400px' }, + ); - const dialogRef = this.dialog.open(MetadataDialogComponent, { - data: { - metadata: this.metadata, - setFcn: setFcn, - removeFcn: removeFcn, + dialogRefPhone + .afterClosed() + .pipe( + map((resp) => formatPhone(resp?.value)), + filter(Boolean), + switchMap(({ phone }) => this.userService.setPhone({ userId: user.userId, phone })), + ) + .subscribe({ + next: () => { + this.toast.showInfo('USER.TOAST.PHONESAVED', true); + this.refreshChanges$.emit(); + }, + error: (error) => { + this.toast.showError(error); }, }); + } - dialogRef.afterClosed().subscribe(() => { - this.loadMetadata(this.user.id); + private getMetadataById(userId: string): Observable { + return defer(() => this.newMgmtService.listUserMetadata(userId)).pipe( + map((metadata) => ({ state: 'success', value: metadata.result }) as const), + startWith({ state: 'loading', value: [] as Metadata[] } as const), + catchError((err) => of({ state: 'error', value: err.message ?? '' } as const)), + ); + } + + public editMetadata(user: UserV2, metadata: Metadata[]): void { + const setFcn = (key: string, value: string) => + this.newMgmtService.setUserMetadata({ + key, + value: Buffer.from(value), + id: user.userId, }); + const removeFcn = (key: string): Promise => this.newMgmtService.removeUserMetadata({ key, id: user.userId }); + + const dialogRef = this.dialog.open(MetadataDialogComponent, { + data: { + metadata: [...metadata], + setFcn: setFcn, + removeFcn: removeFcn, + }, + }); + + dialogRef + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.refreshMetadata$.next(true); + }); + } + + public humanUser(user: UserV2): UserWithHumanType | undefined { + if (user.type.case === 'human') { + return { ...user, type: user.type }; } + return undefined; } } diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html index a6d0bf652f..6d0f1bbdf0 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html @@ -1,35 +1,34 @@ - + - -
- {{ mfa?.name }} + {{ mfa.name }}
+ +
@@ -40,8 +39,8 @@ {{ 'USER.MFA.STATE.' + mfa.state | translate }} @@ -58,7 +57,7 @@ matTooltip="{{ 'ACTIONS.REMOVE' | translate }}" color="warn" mat-icon-button - (click)="deleteMFA(mfa)" + (click)="deleteMFA(mfaQuery.user, mfa)" > @@ -69,13 +68,13 @@
{{ 'USER.MFA.TABLETYPE' | translate }} - TOTP (Time-based One-Time Password) - U2F (Universal 2nd Factor) - One-Time Password SMS - One-Time Password Email + TOTP (Time-based One-Time Password) + U2F (Universal 2nd Factor) + One-Time Password SMS + One-Time Password Email {{ 'USER.MFA.NAME' | translate }} - - {{ mfa.u2f.name }} + + {{ mfa.type.value.name }}
-
+
{{ 'USER.MFA.EMPTY' | translate }}
-
+
diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts index 106c6697d0..1a0bbf0ae1 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts @@ -1,65 +1,104 @@ -import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, Input, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; -import { MatTable, MatTableDataSource } from '@angular/material/table'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { MatTableDataSource } from '@angular/material/table'; +import { combineLatestWith, defer, EMPTY, Observable, ReplaySubject, Subject, switchMap } from 'rxjs'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; -import { AuthFactor, AuthFactorState, User } from 'src/app/proto/generated/zitadel/user_pb'; -import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; +import { UserService } from 'src/app/services/user.service'; +import { AuthFactor, AuthFactorState, User } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { catchError, filter, map, startWith } from 'rxjs/operators'; +import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith'; export interface MFAItem { name: string; verified: boolean; } +type MFAQuery = + | { state: 'success'; value: MatTableDataSource; user: User } + | { state: 'loading'; value: MatTableDataSource; user: User }; + @Component({ selector: 'cnsl-user-mfa', templateUrl: './user-mfa.component.html', styleUrls: ['./user-mfa.component.scss'], }) -export class UserMfaComponent implements OnInit, OnDestroy { - public displayedColumns: string[] = ['type', 'name', 'state', 'actions']; - @Input() public user!: User.AsObject; - public mfaSubject: BehaviorSubject = new BehaviorSubject([]); - private loadingSubject: BehaviorSubject = new BehaviorSubject(false); - public loading$: Observable = this.loadingSubject.asObservable(); +export class UserMfaComponent { + @Input({ required: true }) public set user(user: User) { + this.user$.next(user); + } - @ViewChild(MatTable) public table!: MatTable; @ViewChild(MatSort) public sort!: MatSort; - public dataSource: MatTableDataSource = new MatTableDataSource([]); + public dataSource = new MatTableDataSource([]); - public AuthFactorState: any = AuthFactorState; + public displayedColumns: string[] = ['type', 'name', 'state', 'actions']; + private user$ = new ReplaySubject(1); + public mfaQuery$: Observable; + public refresh$ = new Subject(); + public AuthFactorState = AuthFactorState; - public error: string = ''; constructor( - private mgmtUserService: ManagementService, - private dialog: MatDialog, - private toast: ToastService, - ) {} - - public ngOnInit(): void { - this.getMFAs(); + private readonly dialog: MatDialog, + private readonly toast: ToastService, + private readonly userService: UserService, + ) { + this.mfaQuery$ = this.user$.pipe( + combineLatestWith(this.refresh$.pipe(startWith(true))), + switchMap(([user]) => this.listAuthenticationFactors(user)), + pairwiseStartWith(undefined), + map(([prev, curr]) => { + if (prev?.state === 'success' && curr.state === 'loading') { + return { ...prev, state: 'loading' } as const; + } + return curr; + }), + catchError((error) => { + this.toast.showError(error); + return EMPTY; + }), + ); } - ngOnDestroy(): void { - this.mfaSubject.complete(); - this.loadingSubject.complete(); + private listAuthenticationFactors(user: User): Observable { + return defer(() => this.userService.listAuthenticationFactors({ userId: user.userId })).pipe( + map( + ({ result }) => + ({ + state: 'success', + value: new MatTableDataSource(result), + user, + }) as const, + ), + startWith({ + state: 'loading', + value: new MatTableDataSource([]), + user, + } as const), + ); } - public getMFAs(): void { - this.mgmtUserService - .listHumanMultiFactors(this.user.id) - .then((mfas) => { - this.dataSource = new MatTableDataSource(mfas.resultList); - this.dataSource.sort = this.sort; - }) - .catch((error) => { - this.error = error.message; - }); + private async removeTOTP(user: User) { + await this.userService.removeTOTP(user.userId); + return ['USER.TOAST.OTPREMOVED', 'otp'] as const; } - public deleteMFA(factor: AuthFactor.AsObject): void { + private async removeU2F(user: User, u2fId: string) { + await this.userService.removeU2F(user.userId, u2fId); + return ['USER.TOAST.U2FREMOVED', 'u2f'] as const; + } + + private async removeOTPEmail(user: User) { + await this.userService.removeOTPEmail(user.userId); + return ['USER.TOAST.OTPREMOVED', 'otpEmail'] as const; + } + + private async removeOTPSMS(user: User) { + await this.userService.removeOTPSMS(user.userId); + return ['USER.TOAST.OTPREMOVED', 'otpSms'] as const; + } + + public deleteMFA(user: User, factor: AuthFactor): void { const dialogRef = this.dialog.open(WarnDialogComponent, { data: { confirmKey: 'ACTIONS.DELETE', @@ -70,70 +109,35 @@ export class UserMfaComponent implements OnInit, OnDestroy { width: '400px', }); - dialogRef.afterClosed().subscribe((resp) => { - if (resp) { - if (factor.otp) { - this.mgmtUserService - .removeHumanMultiFactorOTP(this.user.id) - .then(() => { - this.toast.showInfo('USER.TOAST.OTPREMOVED', true); - - const index = this.dataSource.data.findIndex((mfa) => !!mfa.otp); - if (index > -1) { - this.dataSource.data.splice(index, 1); - } - this.getMFAs(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } else if (factor.u2f) { - this.mgmtUserService - .removeHumanAuthFactorU2F(this.user.id, factor.u2f.id) - .then(() => { - this.toast.showInfo('USER.TOAST.U2FREMOVED', true); - - const index = this.dataSource.data.findIndex((mfa) => !!mfa.u2f); - if (index > -1) { - this.dataSource.data.splice(index, 1); - } - this.getMFAs(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } else if (factor.otpEmail) { - this.mgmtUserService - .removeHumanAuthFactorOTPEmail(this.user.id) - .then(() => { - this.toast.showInfo('USER.TOAST.OTPREMOVED', true); - - const index = this.dataSource.data.findIndex((mfa) => !!mfa.otpEmail); - if (index > -1) { - this.dataSource.data.splice(index, 1); - } - this.getMFAs(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } else if (factor.otpSms) { - this.mgmtUserService - .removeHumanAuthFactorOTPSMS(this.user.id) - .then(() => { - this.toast.showInfo('USER.TOAST.OTPREMOVED', true); - - const index = this.dataSource.data.findIndex((mfa) => !!mfa.otpSms); - if (index > -1) { - this.dataSource.data.splice(index, 1); - } - this.getMFAs(); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - } - }); + dialogRef + .afterClosed() + .pipe( + filter(Boolean), + switchMap(() => { + switch (factor.type.case) { + case 'otp': + return this.removeTOTP(user); + case 'u2f': + return this.removeU2F(user, factor.type.value.id); + case 'otpEmail': + return this.removeOTPEmail(user); + case 'otpSms': + return this.removeOTPSMS(user); + default: + throw new Error('Unknown MFA type'); + } + }), + ) + .subscribe({ + next: ([translation, caseId]) => { + this.toast.showInfo(translation, true); + const index = this.dataSource.data.findIndex((mfa) => mfa.type.case === caseId); + if (index > -1) { + this.dataSource.data.splice(index, 1); + } + this.refresh$.next(true); + }, + error: (error) => this.toast.showError(error), + }); } } diff --git a/console/src/app/pages/users/user-list/user-list.component.html b/console/src/app/pages/users/user-list/user-list.component.html index b7d2110245..132b5b7942 100644 --- a/console/src/app/pages/users/user-list/user-list.component.html +++ b/console/src/app/pages/users/user-list/user-list.component.html @@ -1,5 +1,5 @@
-
+

{{ 'DESCRIPTIONS.USERS.TITLE' | translate }}

@@ -7,21 +7,6 @@

{{ 'DESCRIPTIONS.USERS.DESCRIPTION' | translate }}

- - - - - - - - +
diff --git a/console/src/app/pages/users/user-list/user-list.component.ts b/console/src/app/pages/users/user-list/user-list.component.ts index 9773f89dd6..ffb0b96b64 100644 --- a/console/src/app/pages/users/user-list/user-list.component.ts +++ b/console/src/app/pages/users/user-list/user-list.component.ts @@ -1,8 +1,5 @@ import { Component } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { take } from 'rxjs/operators'; -import { Type } from 'src/app/proto/generated/zitadel/user_pb'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; @Component({ @@ -11,23 +8,10 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/ styleUrls: ['./user-list.component.scss'], }) export class UserListComponent { - public Type: any = Type; - public type: Type = Type.TYPE_HUMAN; - constructor( - public translate: TranslateService, - activatedRoute: ActivatedRoute, + protected readonly translate: TranslateService, breadcrumbService: BreadcrumbService, ) { - activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => { - const { type } = params; - if (type && type === 'human') { - this.type = Type.TYPE_HUMAN; - } else if (type && type === 'machine') { - this.type = Type.TYPE_MACHINE; - } - }); - const bread: Breadcrumb = { type: BreadcrumbType.ORG, routerLink: ['/org'], diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.html b/console/src/app/pages/users/user-list/user-table/user-table.component.html index cc81a4d4c0..af293c01a4 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.html +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.html @@ -1,31 +1,29 @@

{{ - (type === Type.TYPE_HUMAN ? 'DESCRIPTIONS.USERS.HUMANS.DESCRIPTION' : 'DESCRIPTIONS.USERS.MACHINES.DESCRIPTION') - | translate + (type === Type.HUMAN ? 'DESCRIPTIONS.USERS.HUMANS.DESCRIPTION' : 'DESCRIPTIONS.USERS.MACHINES.DESCRIPTION') | translate }}

@@ -61,12 +59,12 @@
@@ -86,7 +84,7 @@
- +
- - - - - - - - - - @@ -242,27 +220,25 @@ - +
@@ -115,10 +113,10 @@ [checked]="selection.isSelected(user)" > @@ -134,87 +132,67 @@ -
+ {{ 'USER.PROFILE.DISPLAYNAME' | translate }} - {{ user.human.profile?.displayName }} - {{ user.machine.name }} + + {{ user.type.value?.profile?.displayName }} + {{ user.type.value.name }} + {{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }} - {{ user.preferredLoginName }} + + {{ user.preferredLoginName }} + {{ 'USER.PROFILE.USERNAME' | translate }} - {{ user.userName }} + + {{ user.username }} + {{ 'USER.EMAIL' | translate }} - {{ user.human?.email.email }} + + {{ user.type.value.email.email }} {{ 'USER.DATA.STATE' | translate }} + - {{ 'USER.DATA.STATE' + user.state | translate }} + {{ 'USER.STATEV2.' + user.state | translate }} {{ 'USER.TABLE.CREATIONDATE' | translate }} + {{ user.details.creationDate | timestampToDate | localizedDate: 'regular' }} {{ 'USER.TABLE.CHANGEDATE' | translate }} + {{ user.details.changeDate | timestampToDate | localizedDate: 'regular' }}
-
+
{{ 'USER.TABLE.EMPTY' | translate }}
+ diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.ts b/console/src/app/pages/users/user-list/user-table/user-table.component.ts index 2eb694866e..6ff0d2cd67 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.ts +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.ts @@ -1,30 +1,45 @@ -import { LiveAnnouncer } from '@angular/cdk/a11y'; import { SelectionModel } from '@angular/cdk/collections'; -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, signal, Signal, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { MatSort, Sort } from '@angular/material/sort'; +import { MatSort, SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, Observable, of } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { + combineLatestWith, + defer, + delay, + distinctUntilChanged, + EMPTY, + from, + Observable, + of, + ReplaySubject, + shareReplay, + switchMap, + toArray, +} from 'rxjs'; +import { catchError, filter, finalize, map, startWith, take } from 'rxjs/operators'; import { enterAnimations } from 'src/app/animations'; import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component'; -import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component'; +import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; -import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb'; -import { SearchQuery, Type, TypeQuery, User, UserFieldName, UserState } from 'src/app/proto/generated/zitadel/user_pb'; -import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; -import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; +import { UserService } from 'src/app/services/user.service'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { SearchQuery as UserSearchQuery } from 'src/app/proto/generated/zitadel/user_pb'; +import { Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb'; +import { UserState, User } from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { ListUsersRequestSchema, ListUsersResponse } from '@zitadel/proto/zitadel/user/v2/user_service_pb'; +import { AuthenticationService } from 'src/app/services/authentication.service'; +import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; +import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb'; -enum UserListSearchKey { - FIRST_NAME, - LAST_NAME, - DISPLAY_NAME, - USER_NAME, - EMAIL, -} +type Query = Exclude< + Exclude['queries'], undefined>[number]['query'], + undefined +>; @Component({ selector: 'cnsl-user-table', @@ -33,23 +48,33 @@ enum UserListSearchKey { animations: [enterAnimations], }) export class UserTableComponent implements OnInit { - public userSearchKey: UserListSearchKey | undefined = undefined; - public Type: any = Type; - @Input() public type: Type = Type.TYPE_HUMAN; - @Input() refreshOnPreviousRoutes: string[] = []; + protected readonly Type = Type; + protected readonly refresh$ = new ReplaySubject(1); + @Input() public canWrite$: Observable = of(false); @Input() public canDelete$: Observable = of(false); - @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; - @ViewChild(MatSort) public sort!: MatSort; - public INITIAL_PAGE_SIZE: number = 20; + protected readonly dataSize: Signal; + protected readonly loading = signal(false); + + private readonly paginator$ = new ReplaySubject(1); + @ViewChild(PaginatorComponent) public set paginator(paginator: PaginatorComponent) { + this.paginator$.next(paginator); + } + private readonly sort$ = new ReplaySubject(1); + @ViewChild(MatSort) public set sort(sort: MatSort) { + this.sort$.next(sort); + } + + protected readonly INITIAL_PAGE_SIZE = 20; + + protected readonly dataSource: MatTableDataSource = new MatTableDataSource(); + protected readonly selection: SelectionModel = new SelectionModel(true, []); + protected readonly users$: Observable; + protected readonly type$: Observable; + protected readonly searchQueries$ = new ReplaySubject(1); + protected readonly myUser: Signal; - public viewTimestamp!: Timestamp.AsObject; - public totalResult: number = 0; - public dataSource: MatTableDataSource = new MatTableDataSource(); - public selection: SelectionModel = new SelectionModel(true, []); - private loadingSubject: BehaviorSubject = new BehaviorSubject(false); - public loading$: Observable = this.loadingSubject.asObservable(); @Input() public displayedColumnsHuman: string[] = [ 'select', 'displayName', @@ -70,56 +95,256 @@ export class UserTableComponent implements OnInit { 'actions', ]; - @Output() public changedSelection: EventEmitter> = new EventEmitter(); + @Output() public changedSelection: EventEmitter> = new EventEmitter(); - public UserState: any = UserState; - public UserListSearchKey: any = UserListSearchKey; + protected readonly UserState = UserState; - public ActionKeysType: any = ActionKeysType; - public filterOpen: boolean = false; + protected ActionKeysType = ActionKeysType; + protected filterOpen: boolean = false; - private searchQueries: SearchQuery[] = []; constructor( - private router: Router, - public translate: TranslateService, - private authService: GrpcAuthService, - private userService: ManagementService, - private toast: ToastService, - private dialog: MatDialog, - private route: ActivatedRoute, - private _liveAnnouncer: LiveAnnouncer, + protected readonly router: Router, + public readonly translate: TranslateService, + private readonly userService: UserService, + private readonly toast: ToastService, + private readonly dialog: MatDialog, + private readonly route: ActivatedRoute, + private readonly destroyRef: DestroyRef, + private readonly authenticationService: AuthenticationService, + private readonly authService: GrpcAuthService, ) { - this.selection.changed.subscribe(() => { - this.changedSelection.emit(this.selection.selected); - }); + this.type$ = this.getType$().pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.users$ = this.getUsers(this.type$).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.myUser = toSignal(this.getMyUser()); + + this.dataSize = toSignal( + this.users$.pipe( + map((users) => Number(users.details?.totalResult ?? users.result.length)), + distinctUntilChanged(), + ), + { initialValue: 0 }, + ); } ngOnInit(): void { - this.route.queryParams.pipe(take(1)).subscribe((params) => { - if (!params['filter']) { - this.getData(this.INITIAL_PAGE_SIZE, 0, this.type, this.searchQueries); - } - - if (params['deferredReload']) { - setTimeout(() => { - this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type); - }, 2000); - } + this.selection.changed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.changedSelection.emit(this.selection.selected); }); + + this.users$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((users) => (this.dataSource.data = users.result)); + + this.route.queryParamMap + .pipe( + map((params) => params.get('deferredReload')), + filter(Boolean), + take(1), + delay(2000), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(() => this.refresh$.next(true)); } - public setType(type: Type): void { - this.type = type; - this.router.navigate([], { - relativeTo: this.route, - queryParams: { - type: type === Type.TYPE_HUMAN ? 'human' : type === Type.TYPE_MACHINE ? 'machine' : 'human', - }, - replaceUrl: true, - queryParamsHandling: 'merge', - skipLocationChange: false, - }); - this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type, this.searchQueries); + setType(type: Type) { + this.router + .navigate([], { + relativeTo: this.route, + queryParams: { + type: type === Type.HUMAN ? 'human' : type === Type.MACHINE ? 'machine' : 'human', + }, + replaceUrl: true, + queryParamsHandling: 'merge', + skipLocationChange: false, + }) + .then(); + } + + private getMyUser() { + return this.userService.user$.pipe( + catchError((error) => { + this.toast.showError(error); + return EMPTY; + }), + ); + } + + private getType$(): Observable { + return this.route.queryParamMap.pipe( + map((params) => params.get('type')), + filter(Boolean), + map((type) => (type === 'machine' ? Type.MACHINE : Type.HUMAN)), + startWith(Type.HUMAN), + distinctUntilChanged(), + ); + } + + private getDirection$() { + return this.sort$.pipe( + switchMap((sort) => + sort.sortChange.pipe( + map(({ direction }) => direction), + startWith(sort.direction), + ), + ), + distinctUntilChanged(), + ); + } + + private getSortingColumn$() { + return this.sort$.pipe( + switchMap((sort) => + sort.sortChange.pipe( + map(({ active }) => active), + startWith(sort.active), + ), + ), + map((active) => { + switch (active) { + case 'displayName': + return UserFieldName.DISPLAY_NAME; + case 'username': + return UserFieldName.USER_NAME; + case 'preferredLoginName': + // TODO: replace with preferred username sorting once implemented + return UserFieldName.USER_NAME; + case 'email': + return UserFieldName.EMAIL; + case 'state': + return UserFieldName.STATE; + case 'creationDate': + return UserFieldName.CREATION_DATE; + default: + return undefined; + } + }), + distinctUntilChanged(), + ); + } + + private getQueries(type$: Observable): Observable { + const activeOrgId$ = this.getActiveOrgId(); + + return this.searchQueries$.pipe( + startWith([]), + combineLatestWith(type$, activeOrgId$), + switchMap(([queries, type, organizationId]) => + from(queries).pipe( + map((query) => this.searchQueryToV2(query.toObject())), + startWith({ case: 'typeQuery' as const, value: { type } }), + startWith(organizationId ? { case: 'organizationIdQuery' as const, value: { organizationId } } : undefined), + filter(Boolean), + toArray(), + ), + ), + ); + } + + private searchQueryToV2(query: UserSearchQuery.AsObject): Query | undefined { + if (query.userNameQuery) { + return { + case: 'userNameQuery' as const, + value: { + userName: query.userNameQuery.userName, + method: query.userNameQuery.method as unknown as any, + }, + }; + } else if (query.displayNameQuery) { + return { + case: 'displayNameQuery' as const, + value: { + displayName: query.displayNameQuery.displayName, + method: query.displayNameQuery.method as unknown as any, + }, + }; + } else if (query.emailQuery) { + return { + case: 'emailQuery' as const, + value: { + emailAddress: query.emailQuery.emailAddress, + method: query.emailQuery.method as unknown as any, + }, + }; + } else if (query.stateQuery) { + return { + case: 'stateQuery' as const, + value: { + state: this.toV2State(query.stateQuery.state), + }, + }; + } else { + return undefined; + } + } + + private toV2State(state: UserStateV1) { + switch (state) { + case UserStateV1.USER_STATE_ACTIVE: + return UserState.ACTIVE; + case UserStateV1.USER_STATE_INACTIVE: + return UserState.INACTIVE; + case UserStateV1.USER_STATE_DELETED: + return UserState.DELETED; + case UserStateV1.USER_STATE_LOCKED: + return UserState.LOCKED; + case UserStateV1.USER_STATE_INITIAL: + return UserState.INITIAL; + default: + throw new Error(`Invalid UserState ${state}`); + } + } + + private getUsers(type$: Observable) { + const queries$ = this.getQueries(type$); + const direction$ = this.getDirection$(); + const sortingColumn$ = this.getSortingColumn$(); + + const page$ = this.paginator$.pipe(switchMap((paginator) => paginator.page)); + const pageSize$ = page$.pipe( + map(({ pageSize }) => pageSize), + startWith(this.INITIAL_PAGE_SIZE), + distinctUntilChanged(), + ); + const pageIndex$ = page$.pipe( + map(({ pageIndex }) => pageIndex), + startWith(0), + distinctUntilChanged(), + ); + + return this.refresh$.pipe( + startWith(true), + combineLatestWith(queries$, direction$, sortingColumn$, pageSize$, pageIndex$), + switchMap(([_, queries, direction, sortingColumn, pageSize, pageIndex]) => { + return this.fetchUsers(queries, direction, sortingColumn, pageSize, pageIndex); + }), + ); + } + + private fetchUsers( + queries: Query[], + direction: SortDirection, + sortingColumn: UserFieldName | undefined, + pageSize: number, + pageIndex: number, + ) { + return defer(() => { + const req = { + query: { + limit: pageSize, + offset: BigInt(pageIndex * pageSize), + asc: direction === 'asc', + }, + sortingColumn, + queries: queries.map((query) => ({ query })), + }; + + this.loading.set(true); + return this.userService.listUsers(req); + }).pipe( + catchError((error) => { + this.toast.showError(error); + return EMPTY; + }), + finalize(() => this.loading.set(false)), + ); } public isAllSelected(): boolean { @@ -132,138 +357,49 @@ export class UserTableComponent implements OnInit { this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row)); } - public changePage(event: PageEvent): void { - this.selection.clear(); - this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type, this.searchQueries); - } - - public deactivateSelectedUsers(): void { - Promise.all( - this.selection.selected - .filter((u) => u.state === UserState.USER_STATE_ACTIVE) - .map((value) => { - return this.userService.deactivateUser(value.id); - }), - ) - .then(() => { - this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true); - this.selection.clear(); - setTimeout(() => { - this.refreshPage(); - }, 1000); - }) - .catch((error) => { - this.toast.showError(error); + public async deactivateSelectedUsers(): Promise { + const usersToDeactivate = this.selection.selected + .filter((u) => u.state === UserState.ACTIVE) + .map((value) => { + return this.userService.deactivateUser(value.userId); }); - } - public reactivateSelectedUsers(): void { - Promise.all( - this.selection.selected - .filter((u) => u.state === UserState.USER_STATE_INACTIVE) - .map((value) => { - return this.userService.reactivateUser(value.id); - }), - ) - .then(() => { - this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true); - this.selection.clear(); - setTimeout(() => { - this.refreshPage(); - }, 1000); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - - public gotoRouterLink(rL: any): void { - this.router.navigate(rL); - } - - private async getData(limit: number, offset: number, type: Type, searchQueries?: SearchQuery[]): Promise { - this.loadingSubject.next(true); - - let queryT = new SearchQuery(); - const typeQuery = new TypeQuery(); - typeQuery.setType(type); - queryT.setTypeQuery(typeQuery); - - let sortingField: UserFieldName | undefined = undefined; - if (this.sort?.active && this.sort?.direction) - switch (this.sort.active) { - case 'displayName': - sortingField = UserFieldName.USER_FIELD_NAME_DISPLAY_NAME; - break; - case 'username': - sortingField = UserFieldName.USER_FIELD_NAME_USER_NAME; - break; - case 'preferredLoginName': - // TODO: replace with preferred username sorting once implemented - sortingField = UserFieldName.USER_FIELD_NAME_USER_NAME; - break; - case 'email': - sortingField = UserFieldName.USER_FIELD_NAME_EMAIL; - break; - case 'state': - sortingField = UserFieldName.USER_FIELD_NAME_STATE; - break; - case 'creationDate': - sortingField = UserFieldName.USER_FIELD_NAME_CREATION_DATE; - break; - } - - this.userService - .listUsers( - limit, - offset, - searchQueries?.length ? [queryT, ...searchQueries] : [queryT], - sortingField, - this.sort?.direction, - ) - .then((resp) => { - if (resp.details?.totalResult) { - this.totalResult = resp.details?.totalResult; - } else { - this.totalResult = 0; - } - if (resp.details?.viewTimestamp) { - this.viewTimestamp = resp.details?.viewTimestamp; - } - this.dataSource.data = resp.resultList; - this.loadingSubject.next(false); - }) - .catch((error) => { - this.toast.showError(error); - this.loadingSubject.next(false); - }); - } - - public refreshPage(): void { - this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type, this.searchQueries); - } - - public sortChange(sortState: Sort) { - if (sortState.direction && sortState.active) { - this._liveAnnouncer.announce(`Sorted ${sortState.direction} ending`); - this.refreshPage(); - } else { - this._liveAnnouncer.announce('Sorting cleared'); + try { + await Promise.all(usersToDeactivate); + } catch (error) { + this.toast.showError(error); + return; } - } - public applySearchQuery(searchQueries: SearchQuery[]): void { + this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true); this.selection.clear(); - this.searchQueries = searchQueries; - this.getData( - this.paginator ? this.paginator.pageSize : this.INITIAL_PAGE_SIZE, - this.paginator ? this.paginator.pageIndex * this.paginator.pageSize : 0, - this.type, - searchQueries, - ); + setTimeout(() => { + this.refresh$.next(true); + }, 1000); } - public deleteUser(user: User.AsObject): void { + public async reactivateSelectedUsers(): Promise { + const usersToReactivate = this.selection.selected + .filter((u) => u.state === UserState.INACTIVE) + .map((value) => { + return this.userService.reactivateUser(value.userId); + }); + + try { + await Promise.all(usersToReactivate); + } catch (error) { + this.toast.showError(error); + return; + } + + this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true); + this.selection.clear(); + setTimeout(() => { + this.refresh$.next(true); + }, 1000); + } + + public deleteUser(user: User): void { const authUserData = { confirmKey: 'ACTIONS.DELETE', cancelKey: 'ACTIONS.CANCEL', @@ -286,9 +422,10 @@ export class UserTableComponent implements OnInit { confirmation: user.preferredLoginName, }; - if (user && user.id) { - const authUser = this.authService.userSubject.getValue(); - const isMe = authUser?.id === user.id; + if (user?.userId) { + const authUser = this.myUser(); + console.log('my user', authUser); + const isMe = authUser?.userId === user.userId; let dialogRef; @@ -304,32 +441,48 @@ export class UserTableComponent implements OnInit { }); } - dialogRef.afterClosed().subscribe((resp) => { - if (resp) { - this.userService - .removeUser(user.id) - .then(() => { - setTimeout(() => { - this.refreshPage(); - }, 1000); - this.selection.clear(); - this.toast.showInfo('USER.TOAST.DELETED', true); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - }); + dialogRef + .afterClosed() + .pipe( + filter(Boolean), + switchMap(() => this.userService.deleteUser(user.userId)), + ) + .subscribe({ + next: () => { + setTimeout(() => { + this.refresh$.next(true); + }, 1000); + this.selection.clear(); + this.toast.showInfo('USER.TOAST.DELETED', true); + }, + error: (err) => this.toast.showError(err), + }); } } public get multipleActivatePossible(): boolean { const selected = this.selection.selected; - return selected ? selected.findIndex((user) => user.state !== UserState.USER_STATE_ACTIVE) > -1 : false; + return selected ? selected.findIndex((user) => user.state !== UserState.ACTIVE) > -1 : false; } public get multipleDeactivatePossible(): boolean { const selected = this.selection.selected; - return selected ? selected.findIndex((user) => user.state !== UserState.USER_STATE_INACTIVE) > -1 : false; + return selected ? selected.findIndex((user) => user.state !== UserState.INACTIVE) > -1 : false; + } + + private getActiveOrgId() { + return this.authenticationService.authenticationChanged.pipe( + startWith(true), + filter(Boolean), + switchMap(() => + from(this.authService.getActiveOrg()).pipe( + catchError((err) => { + this.toast.showError(err); + return of(undefined); + }), + ), + ), + map((org) => org?.id), + ); } } diff --git a/console/src/app/pages/users/users-routing.module.ts b/console/src/app/pages/users/users-routing.module.ts index 240d32638e..73551a8cc1 100644 --- a/console/src/app/pages/users/users-routing.module.ts +++ b/console/src/app/pages/users/users-routing.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from 'src/app/guards/auth.guard'; -import { RoleGuard } from 'src/app/guards/role.guard'; -import { UserGuard } from 'src/app/guards/user.guard'; +import { authGuard } from 'src/app/guards/auth.guard'; +import { roleGuard } from 'src/app/guards/role-guard'; +import { userGuard } from 'src/app/guards/user-guard'; import { Type } from 'src/app/proto/generated/zitadel/user_pb'; import { AuthUserDetailComponent } from './user-detail/auth-user-detail/auth-user-detail.component'; @@ -22,7 +22,7 @@ const routes: Routes = [ { path: 'create', loadChildren: () => import('./user-create/user-create.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['user.write'], }, @@ -30,7 +30,7 @@ const routes: Routes = [ { path: 'create-machine', loadChildren: () => import('./user-create-machine/user-create-machine.module'), - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['user.write'], }, @@ -38,7 +38,7 @@ const routes: Routes = [ { path: 'me', component: AuthUserDetailComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { animation: 'HomePage', }, @@ -46,13 +46,13 @@ const routes: Routes = [ { path: 'me/password', component: PasswordComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { animation: 'AddPage' }, }, { path: ':id', component: UserDetailComponent, - canActivate: [AuthGuard, UserGuard, RoleGuard], + canActivate: [authGuard, userGuard, roleGuard], data: { roles: ['user.read'], animation: 'HomePage', @@ -61,7 +61,7 @@ const routes: Routes = [ { path: ':id/password', component: PasswordComponent, - canActivate: [AuthGuard, RoleGuard], + canActivate: [authGuard, roleGuard], data: { roles: ['user.write'], animation: 'AddPage', diff --git a/console/src/app/pipes/action-condition-pipe/action-condition-pipe.module.ts b/console/src/app/pipes/action-condition-pipe/action-condition-pipe.module.ts new file mode 100644 index 0000000000..2af211d5bc --- /dev/null +++ b/console/src/app/pipes/action-condition-pipe/action-condition-pipe.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ActionConditionPipe } from './action-condition-pipe.pipe'; + +@NgModule({ + declarations: [ActionConditionPipe], + imports: [CommonModule], + exports: [ActionConditionPipe], +}) +export class ActionConditionPipeModule {} diff --git a/console/src/app/pipes/action-condition-pipe/action-condition-pipe.pipe.ts b/console/src/app/pipes/action-condition-pipe/action-condition-pipe.pipe.ts new file mode 100644 index 0000000000..7c45f9da5f --- /dev/null +++ b/console/src/app/pipes/action-condition-pipe/action-condition-pipe.pipe.ts @@ -0,0 +1,29 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Condition } from '@zitadel/proto/zitadel/action/v2beta/execution_pb'; + +@Pipe({ + name: 'condition', +}) +export class ActionConditionPipe implements PipeTransform { + transform(condition?: Condition): string { + if (!condition?.conditionType?.case) { + return ''; + } + + const conditionType = condition.conditionType.value; + + if ('name' in conditionType) { + // Applies for function condition + return `function: ${conditionType.name}`; + } + + const { condition: innerCondition } = conditionType; + + if (typeof innerCondition.value === 'string') { + // Applies for service, method condition of Request/ResponseCondition, event, and group of EventCondition + return `${innerCondition.case}: ${innerCondition.value}`; + } + + return `all`; + } +} diff --git a/console/src/app/pipes/redirect-pipe/redirect.pipe.ts b/console/src/app/pipes/redirect-pipe/redirect.pipe.ts index 87d7556d4c..4e574dc2d1 100644 --- a/console/src/app/pipes/redirect-pipe/redirect.pipe.ts +++ b/console/src/app/pipes/redirect-pipe/redirect.pipe.ts @@ -11,7 +11,12 @@ export class RedirectPipe implements PipeTransform { uri.startsWith('http://localhost:') || uri.startsWith('http://127.0.0.1') || uri.startsWith('http://[::1]') || - uri.startsWith('http://[0:0:0:0:0:0:0:1]') + uri.startsWith('http://[0:0:0:0:0:0:0:1]') || + uri.startsWith('https://localhost/') || + uri.startsWith('https://localhost:') || + uri.startsWith('https://127.0.0.1') || + uri.startsWith('https://[::1]') || + uri.startsWith('https://[0:0:0:0:0:0:0:1]') ) { return true; } diff --git a/console/src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe.ts b/console/src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe.ts index 19fe058c34..8fadbd30ed 100644 --- a/console/src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe.ts +++ b/console/src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe.ts @@ -1,18 +1,15 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; +import { Timestamp as BufTimestamp } from '@bufbuild/protobuf/wkt'; +import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb'; @Pipe({ name: 'timestampToDate', }) export class TimestampToDatePipe implements PipeTransform { - transform(value: Timestamp.AsObject, ...args: unknown[]): unknown { - return this.dateFromTimestamp(value); - } - - private dateFromTimestamp(date: Timestamp.AsObject): any { - if (date?.seconds !== undefined && date?.nanos !== undefined) { - const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000 / 1000); - return ts; + transform(date: BufTimestamp | Timestamp.AsObject | undefined): Date | undefined { + if (date?.seconds && date.nanos) { + return new Date(Number(date.seconds) * 1000 + date.nanos / 1000 / 1000); } + return undefined; } } diff --git a/console/src/app/services/action.service.ts b/console/src/app/services/action.service.ts new file mode 100644 index 0000000000..dabe2faf01 --- /dev/null +++ b/console/src/app/services/action.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { + CreateTargetRequestSchema, + CreateTargetResponse, + DeleteTargetRequestSchema, + GetTargetRequestSchema, + GetTargetResponse, + ListExecutionFunctionsRequestSchema, + ListExecutionFunctionsResponse, + ListExecutionMethodsRequestSchema, + ListExecutionMethodsResponse, + ListExecutionServicesRequestSchema, + ListExecutionServicesResponse, + ListExecutionsRequestSchema, + ListExecutionsResponse, + ListTargetsRequestSchema, + ListTargetsResponse, + SetExecutionRequestSchema, + SetExecutionResponse, + UpdateTargetRequestSchema, + UpdateTargetResponse, +} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class ActionService { + constructor(private readonly grpcService: GrpcService) {} + + public listTargets(req: MessageInitShape): Promise { + return this.grpcService.actionNew.listTargets(req); + } + + public createTarget(req: MessageInitShape): Promise { + return this.grpcService.actionNew.createTarget(req); + } + + public deleteTarget(req: MessageInitShape): Promise { + return this.grpcService.actionNew.deleteTarget(req); + } + + public getTarget(req: MessageInitShape): Promise { + return this.grpcService.actionNew.getTarget(req); + } + + public updateTarget(req: MessageInitShape): Promise { + return this.grpcService.actionNew.updateTarget(req); + } + + public listExecutionFunctions( + req: MessageInitShape, + ): Promise { + return this.grpcService.actionNew.listExecutionFunctions(req); + } + + public listExecutionMethods( + req: MessageInitShape, + ): Promise { + return this.grpcService.actionNew.listExecutionMethods(req); + } + + public listExecutionServices( + req: MessageInitShape, + ): Promise { + return this.grpcService.actionNew.listExecutionServices(req); + } + + public listExecutions(req: MessageInitShape): Promise { + return this.grpcService.actionNew.listExecutions(req); + } + + public setExecution(req: MessageInitShape): Promise { + return this.grpcService.actionNew.setExecution(req); + } +} diff --git a/console/src/app/services/feature.service.ts b/console/src/app/services/feature.service.ts deleted file mode 100644 index b991971bba..0000000000 --- a/console/src/app/services/feature.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable } from '@angular/core'; -import { GrpcService } from './grpc.service'; - -import { - GetInstanceFeaturesRequest, - GetInstanceFeaturesResponse, - ResetInstanceFeaturesRequest, - SetInstanceFeaturesRequest, - SetInstanceFeaturesResponse, -} from '../proto/generated/zitadel/feature/v2beta/instance_pb'; -import { - GetOrganizationFeaturesRequest, - GetOrganizationFeaturesResponse, -} from '../proto/generated/zitadel/feature/v2beta/organization_pb'; -import { GetUserFeaturesRequest, GetUserFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/user_pb'; -import { GetSystemFeaturesRequest, GetSystemFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/system_pb'; - -@Injectable({ - providedIn: 'root', -}) -export class FeatureService { - constructor(private readonly grpcService: GrpcService) {} - - public getInstanceFeatures(inheritance: boolean): Promise { - const req = new GetInstanceFeaturesRequest(); - req.setInheritance(inheritance); - return this.grpcService.feature.getInstanceFeatures(req, null).then((resp) => resp); - } - - public setInstanceFeatures(req: SetInstanceFeaturesRequest): Promise { - return this.grpcService.feature.setInstanceFeatures(req, null); - } - - public resetInstanceFeatures(): Promise { - const req = new ResetInstanceFeaturesRequest(); - return this.grpcService.feature.resetInstanceFeatures(req, null); - } - - public getOrganizationFeatures(orgId: string, inheritance: boolean): Promise { - const req = new GetOrganizationFeaturesRequest(); - req.setOrganizationId(orgId); - req.setInheritance(inheritance); - return this.grpcService.feature.getOrganizationFeatures(req, null).then((resp) => resp); - } - - public getSystemFeatures(): Promise { - const req = new GetSystemFeaturesRequest(); - return this.grpcService.feature.getSystemFeatures(req, null).then((resp) => resp); - } - - public getUserFeatures(userId: string, inheritance: boolean): Promise { - const req = new GetUserFeaturesRequest(); - req.setInheritance(inheritance); - req.setUserId(userId); - return this.grpcService.feature.getUserFeatures(req, null).then((resp) => resp); - } -} diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index 24feee0ad1..3967f1df06 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { OAuthService } from 'angular-oauth2-oidc'; -import { BehaviorSubject, forkJoin, from, Observable, of, Subject } from 'rxjs'; -import { catchError, distinctUntilChanged, filter, finalize, map, switchMap, timeout, withLatestFrom } from 'rxjs/operators'; +import { BehaviorSubject, combineLatestWith, EMPTY, mergeWith, NEVER, Observable, of, shareReplay, Subject } from 'rxjs'; +import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators'; import { AddMyAuthFactorOTPEmailRequest, @@ -110,41 +110,17 @@ const ORG_LIMIT = 10; }) export class GrpcAuthService { private _activeOrgChanged: Subject = new Subject(); - public user!: Observable; - public userSubject: BehaviorSubject = new BehaviorSubject(undefined); + public user: Observable; private triggerPermissionsRefresh: Subject = new Subject(); - public zitadelPermissions$: Observable = this.triggerPermissionsRefresh.pipe( - switchMap(() => - from(this.listMyZitadelPermissions()).pipe( - map((rolesResp) => rolesResp.resultList), - filter((roles) => !!roles.length), - catchError((_) => { - return of([]); - }), - distinctUntilChanged((a, b) => { - return JSON.stringify(a.sort()) === JSON.stringify(b.sort()); - }), - finalize(() => { - this.fetchedZitadelPermissions.next(true); - }), - ), - ), - ); + public zitadelPermissions: Observable; public labelpolicy$!: Observable; - public labelpolicy: BehaviorSubject = new BehaviorSubject< - LabelPolicy.AsObject | undefined - >(undefined); labelPolicyLoading$: BehaviorSubject = new BehaviorSubject(true); - public privacypolicy$!: Observable; + public privacypolicy$: Observable; public privacypolicy: BehaviorSubject = new BehaviorSubject< PrivacyPolicy.AsObject | undefined >(undefined); - privacyPolicyLoading$: BehaviorSubject = new BehaviorSubject(true); - - public zitadelPermissions: BehaviorSubject = new BehaviorSubject([]); - public readonly fetchedZitadelPermissions: BehaviorSubject = new BehaviorSubject(false); public cachedOrgs: BehaviorSubject = new BehaviorSubject([]); private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {}; @@ -155,74 +131,52 @@ export class GrpcAuthService { private oauthService: OAuthService, private storage: StorageService, ) { - this.zitadelPermissions$.subscribe(this.zitadelPermissions); - this.labelpolicy$ = this.activeOrgChanged.pipe( - switchMap((org) => { - this.labelPolicyLoading$.next(true); - return from(this.getMyLabelPolicy(org ? org.id : '')); - }), + tap(() => this.labelPolicyLoading$.next(true)), + switchMap((org) => this.getMyLabelPolicy(org ? org.id : '')), + tap(() => this.labelPolicyLoading$.next(false)), + finalize(() => this.labelPolicyLoading$.next(false)), filter((policy) => !!policy), + shareReplay({ refCount: true, bufferSize: 1 }), ); - this.labelpolicy$.subscribe({ - next: (policy) => { - this.labelpolicy.next(policy); - this.labelPolicyLoading$.next(false); - }, - error: (error) => { - console.error(error); - this.labelPolicyLoading$.next(false); - }, - }); - this.privacypolicy$ = this.activeOrgChanged.pipe( - switchMap((org) => { - this.privacyPolicyLoading$.next(true); - return from(this.getMyPrivacyPolicy(org ? org.id : '')); - }), + switchMap((org) => this.getMyPrivacyPolicy(org ? org.id : '')), filter((policy) => !!policy), + catchError((err) => { + console.error(err); + return EMPTY; + }), + shareReplay({ refCount: true, bufferSize: 1 }), ); - this.privacypolicy$.subscribe({ - next: (policy) => { - this.privacypolicy.next(policy); - this.privacyPolicyLoading$.next(false); - }, - error: (error) => { - console.error(error); - this.privacyPolicyLoading$.next(false); - }, - }); - - this.user = forkJoin([ - of(this.oauthService.getAccessToken()), - this.oauthService.events.pipe( - filter((e) => e.type === 'token_received'), - timeout(this.oauthService.waitForTokenInMsec || 0), - catchError((_) => of(null)), // timeout is not an error - map((_) => this.oauthService.getAccessToken()), - ), - ]).pipe( - filter((token) => (token ? true : false)), + this.user = this.oauthService.events.pipe( + filter((e) => e.type === 'token_received'), + map(() => this.oauthService.getAccessToken()), + startWith(this.oauthService.getAccessToken()), + filter(Boolean), distinctUntilChanged(), - switchMap(() => { - return from( - this.getMyUser().then((resp) => { - return resp.user; - }), - ); - }), - finalize(() => { - this.loadPermissions(); - }), + switchMap(() => this.getMyUser()), + map((user) => user.user), + shareReplay({ refCount: true, bufferSize: 1 }), ); - this.user.subscribe(this.userSubject); - - this.activeOrgChanged.subscribe(() => { - this.loadPermissions(); - }); + this.zitadelPermissions = this.user.pipe( + combineLatestWith(this.activeOrgChanged), + // ignore errors from observables + catchError(() => of(true)), + // make sure observable never completes + mergeWith(NEVER), + switchMap(() => + this.listMyZitadelPermissions() + .then((resp) => resp.resultList) + .catch(() => []), + ), + distinctUntilChanged((a, b) => { + return JSON.stringify(a.sort()) === JSON.stringify(b.sort()); + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); } public listMyMetadata( @@ -309,7 +263,7 @@ export class GrpcAuthService { } public get activeOrgChanged(): Observable { - return this._activeOrgChanged; + return this._activeOrgChanged.asObservable(); } public setActiveOrg(org: Org.AsObject): void { @@ -328,18 +282,14 @@ export class GrpcAuthService { * @param roles roles of the user */ public isAllowed(roles: string[] | RegExp[], requiresAll: boolean = false): Observable { - if (roles && roles.length > 0) { - return this.fetchedZitadelPermissions.pipe( - withLatestFrom(this.zitadelPermissions), - filter(([hL, p]) => { - return hL === true && !!p.length; - }), - map(([_, zroles]) => this.hasRoles(zroles, roles, requiresAll)), - distinctUntilChanged(), - ); - } else { + if (!roles?.length) { return of(false); } + + return this.zitadelPermissions.pipe( + map((permissions) => this.hasRoles(permissions, roles, requiresAll)), + distinctUntilChanged(), + ); } /** @@ -353,17 +303,14 @@ export class GrpcAuthService { mapper: (attr: any) => string[] | RegExp[], requiresAll: boolean = false, ): Observable { - return this.fetchedZitadelPermissions.pipe( - withLatestFrom(this.zitadelPermissions), - filter(([hL, p]) => { - return hL === true && !!p.length; - }), - map(([_, zroles]) => { - return objects.filter((obj) => { + return this.zitadelPermissions.pipe( + filter((permissions) => !!permissions.length), + map((permissions) => + objects.filter((obj) => { const roles = mapper(obj); - return this.hasRoles(zroles, roles, requiresAll); - }); - }), + return this.hasRoles(permissions, roles, requiresAll); + }), + ), ); } @@ -395,19 +342,6 @@ export class GrpcAuthService { .then((resp) => resp.toObject()); } - public loadMyUser(): void { - from(this.getMyUser()) - .pipe( - map((resp) => resp.user), - catchError((_) => { - return of(undefined); - }), - ) - .subscribe((user) => { - this.userSubject.next(user); - }); - } - public getMyUser(): Promise { return this.grpcService.auth.getMyUser(new GetMyUserRequest(), null).then((resp) => resp.toObject()); } @@ -728,40 +662,39 @@ export class GrpcAuthService { public getMyLabelPolicy(orgIdForCache?: string): Promise { if (orgIdForCache && this.cachedLabelPolicies[orgIdForCache]) { return Promise.resolve(this.cachedLabelPolicies[orgIdForCache]); - } else { - return this.grpcService.auth - .getMyLabelPolicy(new GetMyLabelPolicyRequest(), null) - .then((resp) => resp.toObject()) - .then((resp) => { - if (resp.policy) { - if (orgIdForCache) { - this.cachedLabelPolicies[orgIdForCache] = resp.policy; - } - return Promise.resolve(resp.policy); - } else { - return Promise.reject(); - } - }); } + + return this.grpcService.auth + .getMyLabelPolicy(new GetMyLabelPolicyRequest(), null) + .then((resp) => resp.toObject()) + .then((resp) => { + if (!resp.policy) { + return Promise.reject(); + } + if (orgIdForCache) { + this.cachedLabelPolicies[orgIdForCache] = resp.policy; + } + return resp.policy; + }); } public getMyPrivacyPolicy(orgIdForCache?: string): Promise { if (orgIdForCache && this.cachedPrivacyPolicies[orgIdForCache]) { return Promise.resolve(this.cachedPrivacyPolicies[orgIdForCache]); - } else { - return this.grpcService.auth - .getMyPrivacyPolicy(new GetMyPrivacyPolicyRequest(), null) - .then((resp) => resp.toObject()) - .then((resp) => { - if (resp.policy) { - if (orgIdForCache) { - this.cachedPrivacyPolicies[orgIdForCache] = resp.policy; - } - return Promise.resolve(resp.policy); - } else { - return Promise.reject(); - } - }); } + + return this.grpcService.auth + .getMyPrivacyPolicy(new GetMyPrivacyPolicyRequest(), null) + .then((resp) => resp.toObject()) + .then((resp) => { + if (!resp.policy) { + return Promise.reject(); + } + + if (orgIdForCache) { + this.cachedPrivacyPolicies[orgIdForCache] = resp.policy; + } + return resp.policy; + }); } } diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index 5c4c0fd510..b2f89ca648 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -1,9 +1,8 @@ import { PlatformLocation } from '@angular/common'; import { Injectable } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { AuthConfig } from 'angular-oauth2-oidc'; -import { catchError, switchMap, tap, throwError } from 'rxjs'; +import { catchError, firstValueFrom, switchMap, tap } from 'rxjs'; import { AdminServiceClient } from '../proto/generated/zitadel/AdminServiceClientPb'; import { AuthServiceClient } from '../proto/generated/zitadel/AuthServiceClientPb'; @@ -12,13 +11,29 @@ import { fallbackLanguage, supportedLanguagesRegexp } from '../utils/language'; import { AuthenticationService } from './authentication.service'; import { EnvironmentService } from './environment.service'; import { ExhaustedService } from './exhausted.service'; -import { AuthInterceptor } from './interceptors/auth.interceptor'; +import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor } from './interceptors/auth.interceptor'; import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor'; import { I18nInterceptor } from './interceptors/i18n.interceptor'; -import { OrgInterceptor } from './interceptors/org.interceptor'; +import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor'; import { StorageService } from './storage.service'; -import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2beta/Feature_serviceServiceClientPb'; -import { GrpcAuthService } from './grpc-auth.service'; +import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb'; +//@ts-ignore +import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2'; +//@ts-ignore +import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1'; +import { createGrpcWebTransport } from '@connectrpc/connect-web'; +// @ts-ignore +import { createClientFor } from '@zitadel/client'; +import { Client, Transport } from '@connectrpc/connect'; + +import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb'; +import { ActionService } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; + +// @ts-ignore +import { createClientFor } from '@zitadel/client'; + +const createWebKeyServiceClient = createClientFor(WebKeyService); +const createActionServiceClient = createClientFor(ActionService); @Injectable({ providedIn: 'root', @@ -27,16 +42,24 @@ export class GrpcService { public auth!: AuthServiceClient; public mgmt!: ManagementServiceClient; public admin!: AdminServiceClient; - public feature!: FeatureServiceClient; + public user!: UserServiceClient; + public userNew!: ReturnType; + public session!: ReturnType; + public mgmtNew!: ReturnType; + public authNew!: ReturnType; + public featureNew!: ReturnType; + public actionNew!: ReturnType; + public webKey!: ReturnType; constructor( - private envService: EnvironmentService, - private platformLocation: PlatformLocation, - private authenticationService: AuthenticationService, - private storageService: StorageService, - private dialog: MatDialog, - private translate: TranslateService, - private exhaustedService: ExhaustedService, + private readonly envService: EnvironmentService, + private readonly platformLocation: PlatformLocation, + private readonly authenticationService: AuthenticationService, + private readonly translate: TranslateService, + private readonly exhaustedService: ExhaustedService, + private readonly authInterceptor: AuthInterceptor, + private readonly authInterceptorProvider: AuthInterceptorProvider, + private readonly orgInterceptorProvider: OrgInterceptorProvider, ) {} public loadAppEnvironment(): Promise { @@ -44,66 +67,84 @@ export class GrpcService { const browserLanguage = this.translate.getBrowserLang(); const language = browserLanguage?.match(supportedLanguagesRegexp) ? browserLanguage : fallbackLanguage; - return this.translate - .use(language || this.translate.defaultLang) - .pipe( - switchMap(() => this.envService.env), - tap((env) => { - if (!env?.api || !env?.issuer) { - return; - } - const interceptors = { - unaryInterceptors: [ - new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService), - new OrgInterceptor(this.storageService), - new AuthInterceptor(this.authenticationService, this.storageService, this.dialog), - new I18nInterceptor(this.translate), - ], - }; + const init = this.translate.use(language || this.translate.defaultLang).pipe( + switchMap(() => this.envService.env), + tap((env) => { + if (!env?.api || !env?.issuer) { + return; + } + const interceptors = { + unaryInterceptors: [ + new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService), + new OrgInterceptor(this.orgInterceptorProvider), + this.authInterceptor, + new I18nInterceptor(this.translate), + ], + }; - this.auth = new AuthServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); - this.mgmt = new ManagementServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); - this.admin = new AdminServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); - this.feature = new FeatureServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); + this.auth = new AuthServiceClient( + env.api, + null, + // @ts-ignore + interceptors, + ); + this.mgmt = new ManagementServiceClient( + env.api, + null, + // @ts-ignore + interceptors, + ); + this.admin = new AdminServiceClient( + env.api, + null, + // @ts-ignore + interceptors, + ); + this.user = new UserServiceClient( + env.api, + null, + // @ts-ignore + interceptors, + ); - const authConfig: AuthConfig = { - scope: 'openid profile email', - responseType: 'code', - oidc: true, - clientId: env.clientid, - issuer: env.issuer, - redirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'auth/callback', - postLogoutRedirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'signedout', - requireHttps: false, - }; + const transport = createGrpcWebTransport({ + baseUrl: env.api, + interceptors: [NewConnectWebAuthInterceptor(this.authInterceptorProvider)], + }); + const transportOldAPIs = createGrpcWebTransport({ + baseUrl: env.api, + interceptors: [ + NewConnectWebAuthInterceptor(this.authInterceptorProvider), + NewConnectWebOrgInterceptor(this.orgInterceptorProvider), + ], + }); + this.userNew = createUserServiceClient(transport); + this.session = createSessionServiceClient(transport); + this.mgmtNew = createManagementServiceClient(transportOldAPIs); + this.authNew = createAuthServiceClient(transport); + this.featureNew = createFeatureServiceClient(transport); + this.actionNew = createActionServiceClient(transport); + this.webKey = createWebKeyServiceClient(transport); - this.authenticationService.initConfig(authConfig); - }), - catchError((err) => { - console.error('Failed to load environment from assets', err); - return throwError(() => err); - }), - ) - .toPromise(); + const authConfig: AuthConfig = { + scope: 'openid profile email', + responseType: 'code', + oidc: true, + clientId: env.clientid, + issuer: env.issuer, + redirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'auth/callback', + postLogoutRedirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'signedout', + requireHttps: false, + }; + + this.authenticationService.initConfig(authConfig); + }), + catchError((err) => { + console.error('Failed to load environment from assets', err); + throw err; + }), + ); + + return firstValueFrom(init); } } diff --git a/console/src/app/services/interceptors/auth.interceptor.ts b/console/src/app/services/interceptors/auth.interceptor.ts index 4ccdc768c7..65b7cbc4bf 100644 --- a/console/src/app/services/interceptors/auth.interceptor.ts +++ b/console/src/app/services/interceptors/auth.interceptor.ts @@ -1,59 +1,53 @@ -import { Injectable } from '@angular/core'; +import { DestroyRef, Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { Request, UnaryInterceptor, UnaryResponse } from 'grpc-web'; -import { Subject } from 'rxjs'; -import { debounceTime, filter, first, map, take, tap } from 'rxjs/operators'; +import { Request, RpcError, UnaryInterceptor, UnaryResponse } from 'grpc-web'; +import { firstValueFrom, identity, lastValueFrom, Observable, Subject } from 'rxjs'; +import { debounceTime, filter, map } from 'rxjs/operators'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { AuthenticationService } from '../authentication.service'; import { StorageService } from '../storage.service'; import { AuthConfig } from 'angular-oauth2-oidc'; -import { GrpcAuthService } from '../grpc-auth.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ConnectError, Interceptor } from '@connectrpc/connect'; const authorizationKey = 'Authorization'; const bearerPrefix = 'Bearer'; const accessTokenStorageKey = 'access_token'; + @Injectable({ providedIn: 'root' }) -/** - * Set the authentication token - */ -export class AuthInterceptor implements UnaryInterceptor { - public triggerDialog: Subject = new Subject(); +export class AuthInterceptorProvider { + private readonly triggerDialog: Subject = new Subject(); + constructor( - private authenticationService: AuthenticationService, - private storageService: StorageService, - private dialog: MatDialog, + private readonly authenticationService: AuthenticationService, + private readonly storageService: StorageService, + private readonly dialog: MatDialog, + destroyRef: DestroyRef, ) { - this.triggerDialog.pipe(debounceTime(1000)).subscribe(() => { - this.openDialog(); - }); + this.triggerDialog.pipe(debounceTime(1000), takeUntilDestroyed(destroyRef)).subscribe(() => this.openDialog()); } - public async intercept(request: Request, invoker: any): Promise> { - await this.authenticationService.authenticationChanged - .pipe( - filter((authed) => !!authed), - first(), - ) - .toPromise(); - - const metadata = request.getMetadata(); - const accessToken = this.storageService.getItem(accessTokenStorageKey); - metadata[authorizationKey] = `${bearerPrefix} ${accessToken}`; - - return invoker(request) - .then((response: any) => { - return response; - }) - .catch(async (error: any) => { - if (error.code === 16 || (error.code === 7 && error.message === 'mfa required (AUTHZ-Kl3p0)')) { - this.triggerDialog.next(true); - } - return Promise.reject(error); - }); + getToken(): Observable { + return this.authenticationService.authenticationChanged.pipe( + filter(identity), + map(() => this.storageService.getItem(accessTokenStorageKey)), + map((token) => `${bearerPrefix} ${token}`), + ); } - private openDialog(): void { + handleError = (error: any): never => { + if (!(error instanceof RpcError) && !(error instanceof ConnectError)) { + throw error; + } + + if (error.code === 16 || (error.code === 7 && error.message === 'mfa required (AUTHZ-Kl3p0)')) { + this.triggerDialog.next(true); + } + throw error; + }; + + private async openDialog(): Promise { const dialogRef = this.dialog.open(WarnDialogComponent, { data: { confirmKey: 'ACTIONS.LOGIN', @@ -64,19 +58,47 @@ export class AuthInterceptor implements UnaryIn width: '400px', }); - dialogRef - .afterClosed() - .pipe(take(1)) - .subscribe((resp) => { - if (resp) { - const idToken = this.authenticationService.getIdToken(); - const configWithPrompt: Partial = { - customQueryParams: { - id_token_hint: idToken, - }, - }; - this.authenticationService.authenticate(configWithPrompt, true); - } - }); + const resp = await lastValueFrom(dialogRef.afterClosed()); + if (!resp) { + return; + } + + const idToken = this.authenticationService.getIdToken(); + const configWithPrompt: Partial = { + customQueryParams: { + id_token_hint: idToken, + }, + }; + + await this.authenticationService.authenticate(configWithPrompt, true); } } + +@Injectable({ providedIn: 'root' }) +/** + * Set the authentication token + */ +export class AuthInterceptor implements UnaryInterceptor { + constructor(private readonly authInterceptorProvider: AuthInterceptorProvider) {} + + public async intercept( + request: Request, + invoker: (request: Request) => Promise>, + ): Promise> { + const metadata = request.getMetadata(); + metadata[authorizationKey] = await firstValueFrom(this.authInterceptorProvider.getToken()); + + return invoker(request).catch(this.authInterceptorProvider.handleError); + } +} + +export function NewConnectWebAuthInterceptor(authInterceptorProvider: AuthInterceptorProvider): Interceptor { + return (next) => async (req) => { + if (!req.header.get('Authorization')) { + const token = await firstValueFrom(authInterceptorProvider.getToken()); + req.header.set('Authorization', token); + } + + return next(req).catch(authInterceptorProvider.handleError); + }; +} diff --git a/console/src/app/services/interceptors/exhausted.grpc.interceptor.ts b/console/src/app/services/interceptors/exhausted.grpc.interceptor.ts index 446e7aa8dd..d1b15060c4 100644 --- a/console/src/app/services/interceptors/exhausted.grpc.interceptor.ts +++ b/console/src/app/services/interceptors/exhausted.grpc.interceptor.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; -import { Request, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web'; +import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web'; import { EnvironmentService } from '../environment.service'; import { ExhaustedService } from '../exhausted.service'; +import { lastValueFrom } from 'rxjs'; /** * ExhaustedGrpcInterceptor shows the exhausted dialog after receiving a gRPC response status 8. @@ -17,16 +18,13 @@ export class ExhaustedGrpcInterceptor implement request: Request, invoker: (request: Request) => Promise>, ): Promise> { - return invoker(request).catch((error: any) => { - if (error.code === StatusCode.RESOURCE_EXHAUSTED) { - return this.exhaustedSvc - .showExhaustedDialog(this.envSvc.env) - .toPromise() - .then(() => { - throw error; - }); + try { + return await invoker(request); + } catch (error: any) { + if (error instanceof RpcError && error.code === StatusCode.RESOURCE_EXHAUSTED) { + await lastValueFrom(this.exhaustedSvc.showExhaustedDialog(this.envSvc.env)); } throw error; - }); + } } } diff --git a/console/src/app/services/interceptors/i18n.interceptor.ts b/console/src/app/services/interceptors/i18n.interceptor.ts index af2bb049d1..566508d178 100644 --- a/console/src/app/services/interceptors/i18n.interceptor.ts +++ b/console/src/app/services/interceptors/i18n.interceptor.ts @@ -10,7 +10,7 @@ const i18nHeader = 'Accept-Language'; export class I18nInterceptor implements UnaryInterceptor { constructor(private translate: TranslateService) {} - public async intercept(request: Request, invoker: any): Promise> { + public intercept(request: Request, invoker: any): Promise> { const metadata = request.getMetadata(); const navLang = this.translate.currentLang ?? navigator.language; @@ -18,12 +18,6 @@ export class I18nInterceptor implements UnaryIn metadata[i18nHeader] = navLang; } - return invoker(request) - .then((response: any) => { - return response; - }) - .catch((error: any) => { - return Promise.reject(error); - }); + return invoker(request); } } diff --git a/console/src/app/services/interceptors/org.interceptor.ts b/console/src/app/services/interceptors/org.interceptor.ts index 22d468edd5..e9e9745b12 100644 --- a/console/src/app/services/interceptors/org.interceptor.ts +++ b/console/src/app/services/interceptors/org.interceptor.ts @@ -1,32 +1,65 @@ import { Injectable } from '@angular/core'; -import { Request, UnaryInterceptor, UnaryResponse } from 'grpc-web'; +import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web'; import { Org } from 'src/app/proto/generated/zitadel/org_pb'; import { StorageKey, StorageLocation, StorageService } from '../storage.service'; +import { ConnectError, Interceptor } from '@connectrpc/connect'; +import { firstValueFrom, identity, Observable, Subject } from 'rxjs'; +import { debounceTime, filter, map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; const ORG_HEADER_KEY = 'x-zitadel-orgid'; @Injectable({ providedIn: 'root' }) export class OrgInterceptor implements UnaryInterceptor { - constructor(private readonly storageService: StorageService) {} + constructor(private readonly orgInterceptorProvider: OrgInterceptorProvider) {} - public intercept(request: Request, invoker: any): Promise> { + public async intercept(request: Request, invoker: any): Promise> { const metadata = request.getMetadata(); - const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session); - - if (org) { - metadata[ORG_HEADER_KEY] = `${org.id}`; + const orgId = this.orgInterceptorProvider.getOrgId(); + if (orgId) { + metadata[ORG_HEADER_KEY] = orgId; } - return invoker(request) - .then((response: any) => { - return response; - }) - .catch((error: any) => { - if (error.code === 7 && error.message.startsWith("Organisation doesn't exist")) { - this.storageService.removeItem(StorageKey.organization, StorageLocation.session); - } - return Promise.reject(error); - }); + return invoker(request).catch(this.orgInterceptorProvider.handleError); } } + +export function NewConnectWebOrgInterceptor(orgInterceptorProvider: OrgInterceptorProvider): Interceptor { + return (next) => async (req) => { + if (!req.header.get(ORG_HEADER_KEY)) { + const orgId = orgInterceptorProvider.getOrgId(); + if (orgId) { + req.header.set(ORG_HEADER_KEY, orgId); + } + } + + return next(req).catch(orgInterceptorProvider.handleError); + }; +} + +@Injectable({ providedIn: 'root' }) +export class OrgInterceptorProvider { + constructor(private storageService: StorageService) {} + + getOrgId() { + const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session); + return org?.id; + } + + handleError = (error: any): never => { + if (!(error instanceof RpcError) && !(error instanceof ConnectError)) { + throw error; + } + + if ( + error instanceof RpcError && + error.code === StatusCode.PERMISSION_DENIED && + error.message.startsWith("Organisation doesn't exist") + ) { + this.storageService.removeItem(StorageKey.organization, StorageLocation.session); + } + + throw error; + }; +} diff --git a/console/src/app/services/new-auth.service.ts b/console/src/app/services/new-auth.service.ts new file mode 100644 index 0000000000..4827c0db31 --- /dev/null +++ b/console/src/app/services/new-auth.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import { + AddMyAuthFactorOTPSMSResponse, + GetMyPasswordComplexityPolicyResponse, + GetMyUserResponse, + ListMyMetadataResponse, + VerifyMyPhoneResponse, +} from '@zitadel/proto/zitadel/auth_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class NewAuthService { + constructor(private readonly grpcService: GrpcService) {} + + public getMyUser(): Promise { + return this.grpcService.authNew.getMyUser({}); + } + + public verifyMyPhone(code: string): Promise { + return this.grpcService.authNew.verifyMyPhone({ code }); + } + + public addMyAuthFactorOTPSMS(): Promise { + return this.grpcService.authNew.addMyAuthFactorOTPSMS({}); + } + + public listMyMetadata(): Promise { + return this.grpcService.authNew.listMyMetadata({}); + } + + public getMyPasswordComplexityPolicy(): Promise { + return this.grpcService.authNew.getMyPasswordComplexityPolicy({}); + } +} diff --git a/console/src/app/services/new-feature.service.ts b/console/src/app/services/new-feature.service.ts new file mode 100644 index 0000000000..5bb4fe8dd7 --- /dev/null +++ b/console/src/app/services/new-feature.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import { + GetInstanceFeaturesResponse, + ResetInstanceFeaturesResponse, + SetInstanceFeaturesRequestSchema, + SetInstanceFeaturesResponse, +} from '@zitadel/proto/zitadel/feature/v2/instance_pb'; +import { MessageInitShape } from '@bufbuild/protobuf'; + +@Injectable({ + providedIn: 'root', +}) +export class NewFeatureService { + constructor(private readonly grpcService: GrpcService) {} + + public getInstanceFeatures(): Promise { + return this.grpcService.featureNew.getInstanceFeatures({}); + } + + public setInstanceFeatures( + req: MessageInitShape, + ): Promise { + return this.grpcService.featureNew.setInstanceFeatures(req); + } + + public resetInstanceFeatures(): Promise { + return this.grpcService.featureNew.resetInstanceFeatures({}); + } +} diff --git a/console/src/app/services/new-mgmt.service.ts b/console/src/app/services/new-mgmt.service.ts new file mode 100644 index 0000000000..6798d25f41 --- /dev/null +++ b/console/src/app/services/new-mgmt.service.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import { + GenerateMachineSecretRequestSchema, + GenerateMachineSecretResponse, + GetDefaultPasswordComplexityPolicyResponse, + GetLoginPolicyRequestSchema, + GetLoginPolicyResponse, + GetPasswordComplexityPolicyResponse, + ListUserMetadataRequestSchema, + ListUserMetadataResponse, + RemoveMachineSecretRequestSchema, + RemoveMachineSecretResponse, + RemoveUserMetadataRequestSchema, + RemoveUserMetadataResponse, + ResendHumanEmailVerificationRequestSchema, + ResendHumanEmailVerificationResponse, + ResendHumanInitializationRequestSchema, + ResendHumanInitializationResponse, + ResendHumanPhoneVerificationRequestSchema, + ResendHumanPhoneVerificationResponse, + SendHumanResetPasswordNotificationRequest_Type, + SendHumanResetPasswordNotificationRequestSchema, + SendHumanResetPasswordNotificationResponse, + SetUserMetadataRequestSchema, + SetUserMetadataResponse, + UpdateMachineRequestSchema, + UpdateMachineResponse, +} from '@zitadel/proto/zitadel/management_pb'; +import { MessageInitShape, create } from '@bufbuild/protobuf'; + +@Injectable({ + providedIn: 'root', +}) +export class NewMgmtService { + constructor(private readonly grpcService: GrpcService) {} + + public getLoginPolicy(): Promise { + return this.grpcService.mgmtNew.getLoginPolicy(create(GetLoginPolicyRequestSchema)); + } + + public generateMachineSecret(userId: string): Promise { + return this.grpcService.mgmtNew.generateMachineSecret(create(GenerateMachineSecretRequestSchema, { userId })); + } + + public removeMachineSecret(userId: string): Promise { + return this.grpcService.mgmtNew.removeMachineSecret(create(RemoveMachineSecretRequestSchema, { userId })); + } + + public updateMachine(req: MessageInitShape): Promise { + return this.grpcService.mgmtNew.updateMachine(create(UpdateMachineRequestSchema, req)); + } + + public resendHumanEmailVerification(userId: string): Promise { + return this.grpcService.mgmtNew.resendHumanEmailVerification( + create(ResendHumanEmailVerificationRequestSchema, { userId }), + ); + } + + public resendHumanPhoneVerification(userId: string): Promise { + return this.grpcService.mgmtNew.resendHumanPhoneVerification( + create(ResendHumanPhoneVerificationRequestSchema, { userId }), + ); + } + + public sendHumanResetPasswordNotification( + userId: string, + type: SendHumanResetPasswordNotificationRequest_Type, + ): Promise { + return this.grpcService.mgmtNew.sendHumanResetPasswordNotification( + create(SendHumanResetPasswordNotificationRequestSchema, { userId, type }), + ); + } + + public resendHumanInitialization(userId: string, email: string = ''): Promise { + return this.grpcService.mgmtNew.resendHumanInitialization( + create(ResendHumanInitializationRequestSchema, { userId, email }), + ); + } + + public listUserMetadata(id: string): Promise { + return this.grpcService.mgmtNew.listUserMetadata(create(ListUserMetadataRequestSchema, { id })); + } + + public setUserMetadata(req: MessageInitShape): Promise { + return this.grpcService.mgmtNew.setUserMetadata(create(SetUserMetadataRequestSchema, req)); + } + + public removeUserMetadata( + req: MessageInitShape, + ): Promise { + return this.grpcService.mgmtNew.removeUserMetadata(create(RemoveUserMetadataRequestSchema, req)); + } + + public getPasswordComplexityPolicy(): Promise { + return this.grpcService.mgmtNew.getPasswordComplexityPolicy({}); + } + + public getDefaultPasswordComplexityPolicy(): Promise { + return this.grpcService.mgmtNew.getDefaultPasswordComplexityPolicy({}); + } +} diff --git a/console/src/app/services/password-complexity-validator-factory.service.ts b/console/src/app/services/password-complexity-validator-factory.service.ts new file mode 100644 index 0000000000..82c6d5eb58 --- /dev/null +++ b/console/src/app/services/password-complexity-validator-factory.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { ValidatorFn } from '@angular/forms'; +import { + containsLowerCaseValidator, + containsNumberValidator, + containsSymbolValidator, + containsUpperCaseValidator, + minLengthValidator, + requiredValidator, +} from '../modules/form-field/validators/validators'; +import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class PasswordComplexityValidatorFactoryService { + constructor() {} + + buildValidators(policy?: PasswordComplexityPolicy) { + const validators: ValidatorFn[] = [requiredValidator]; + if (policy?.minLength) { + validators.push(minLengthValidator(Number(policy.minLength))); + } + if (policy?.hasLowercase) { + validators.push(containsLowerCaseValidator); + } + if (policy?.hasUppercase) { + validators.push(containsUpperCaseValidator); + } + if (policy?.hasNumber) { + validators.push(containsNumberValidator); + } + if (policy?.hasSymbol) { + validators.push(containsSymbolValidator); + } + return validators; + } +} diff --git a/console/src/app/services/posthog.service.ts b/console/src/app/services/posthog.service.ts index a3b9b23596..2f9630282a 100644 --- a/console/src/app/services/posthog.service.ts +++ b/console/src/app/services/posthog.service.ts @@ -1,7 +1,7 @@ -import { Injectable, OnDestroy } from '@angular/core'; +import { DestroyRef, Injectable, OnDestroy } from '@angular/core'; import { EnvironmentService } from './environment.service'; -import { Subscription } from 'rxjs'; import posthog from 'posthog-js'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Injectable({ providedIn: 'root', @@ -9,17 +9,16 @@ import posthog from 'posthog-js'; export class PosthogService implements OnDestroy { private posthogToken?: string; private posthogUrl?: string; - private envSubscription: Subscription; - constructor(private envService: EnvironmentService) { - this.envSubscription = this.envService.env.subscribe((env) => { + constructor(envService: EnvironmentService, destroyRef: DestroyRef) { + envService.env.pipe(takeUntilDestroyed(destroyRef)).subscribe((env) => { this.posthogToken = env.posthog_token; this.posthogUrl = env.posthog_url; this.init(); }); } - async init() { + init() { if (this.posthogToken && this.posthogUrl) { posthog.init(this.posthogToken, { api_host: this.posthogUrl, @@ -34,10 +33,6 @@ export class PosthogService implements OnDestroy { } ngOnDestroy() { - if (this.envSubscription) { - this.envSubscription.unsubscribe(); - } - posthog.reset(); } } diff --git a/console/src/app/services/session.service.ts b/console/src/app/services/session.service.ts new file mode 100644 index 0000000000..12e07049b4 --- /dev/null +++ b/console/src/app/services/session.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import type { MessageInitShape } from '@bufbuild/protobuf'; +import { ListSessionsRequestSchema, ListSessionsResponse } from '@zitadel/proto/zitadel/session/v2/session_service_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionService { + constructor(private readonly grpcService: GrpcService) {} + + public listSessions(req: MessageInitShape): Promise { + return this.grpcService.session.listSessions(req); + } +} diff --git a/console/src/app/services/user.service.ts b/console/src/app/services/user.service.ts new file mode 100644 index 0000000000..a5bbd0aaff --- /dev/null +++ b/console/src/app/services/user.service.ts @@ -0,0 +1,322 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import { + AddHumanUserRequestSchema, + AddHumanUserResponse, + CreateInviteCodeRequestSchema, + CreateInviteCodeResponse, + CreatePasskeyRegistrationLinkRequestSchema, + CreatePasskeyRegistrationLinkResponse, + DeactivateUserRequestSchema, + DeactivateUserResponse, + DeleteUserRequestSchema, + DeleteUserResponse, + GetUserByIDResponse, + ListAuthenticationFactorsRequestSchema, + ListAuthenticationFactorsResponse, + ListPasskeysRequestSchema, + ListPasskeysResponse, + ListUsersRequestSchema, + ListUsersResponse, + LockUserRequestSchema, + LockUserResponse, + PasswordResetRequestSchema, + ReactivateUserRequestSchema, + ReactivateUserResponse, + RemoveOTPEmailRequestSchema, + RemoveOTPEmailResponse, + RemoveOTPSMSRequestSchema, + RemoveOTPSMSResponse, + RemovePasskeyRequestSchema, + RemovePasskeyResponse, + RemovePhoneRequestSchema, + RemovePhoneResponse, + RemoveTOTPRequestSchema, + RemoveTOTPResponse, + RemoveU2FRequestSchema, + RemoveU2FResponse, + ResendInviteCodeRequestSchema, + ResendInviteCodeResponse, + SetEmailRequestSchema, + SetEmailResponse, + SetPasswordRequestSchema, + SetPasswordResponse, + SetPhoneRequestSchema, + SetPhoneResponse, + UnlockUserRequestSchema, + UnlockUserResponse, + UpdateHumanUserRequestSchema, + UpdateHumanUserResponse, +} from '@zitadel/proto/zitadel/user/v2/user_service_pb'; +import type { MessageInitShape } from '@bufbuild/protobuf'; +import { + AccessTokenType, + Gender, + HumanProfile, + HumanProfileSchema, + HumanUser, + HumanUserSchema, + MachineUser, + MachineUserSchema, + User as UserV2, + UserSchema, + UserState, +} from '@zitadel/proto/zitadel/user/v2/user_pb'; +import { create } from '@bufbuild/protobuf'; +import { Timestamp as TimestampV2, TimestampSchema } from '@bufbuild/protobuf/wkt'; +import { Details, DetailsSchema } from '@zitadel/proto/zitadel/object/v2/object_pb'; +import { Human, Machine, Phone, Profile, User } from '../proto/generated/zitadel/user_pb'; +import { ObjectDetails } from '../proto/generated/zitadel/object_pb'; +import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb'; +import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { debounceTime, EMPTY, Observable, of, ReplaySubject, shareReplay, switchAll, switchMap } from 'rxjs'; +import { catchError, filter, map, startWith } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + private user$$ = new ReplaySubject>(1); + public user$ = this.user$$.pipe( + startWith(this.getUser()), + // makes sure if many subscribers reset the observable only one wins + debounceTime(10), + switchAll(), + catchError((err) => { + // reset user observable on error + this.user$$.next(this.getUser()); + throw err; + }), + ); + + constructor( + private readonly grpcService: GrpcService, + private readonly oauthService: OAuthService, + ) {} + + private getUserId() { + return this.oauthService.events.pipe( + filter((event) => event.type === 'token_received'), + map(() => this.oauthService.getIdToken()), + startWith(this.oauthService.getIdToken()), + filter(Boolean), + switchMap((token) => { + // we do this in a try catch so the observable will retry this logic if it fails + try { + // split jwt and get base64 encoded payload + const unparsedPayload = atob(token.split('.')[1]); + // parse payload + const payload: unknown = JSON.parse(unparsedPayload); + // check if sub is in payload and is a string + if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') { + return of(payload.sub); + } + return EMPTY; + } catch { + return EMPTY; + } + }), + ); + } + + private getUser() { + return this.getUserId().pipe( + switchMap((id) => this.getUserById(id)), + map((resp) => resp.user), + filter(Boolean), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + } + + public addHumanUser(req: MessageInitShape): Promise { + return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req)); + } + + public listUsers(req: MessageInitShape): Promise { + return this.grpcService.userNew.listUsers(req); + } + + public getUserById(userId: string): Promise { + return this.grpcService.userNew.getUserByID({ userId }); + } + + public deactivateUser(userId: string): Promise { + return this.grpcService.userNew.deactivateUser(create(DeactivateUserRequestSchema, { userId })); + } + + public reactivateUser(userId: string): Promise { + return this.grpcService.userNew.reactivateUser(create(ReactivateUserRequestSchema, { userId })); + } + + public deleteUser(userId: string): Promise { + return this.grpcService.userNew.deleteUser(create(DeleteUserRequestSchema, { userId })); + } + + public updateUser(req: MessageInitShape): Promise { + return this.grpcService.userNew.updateHumanUser(create(UpdateHumanUserRequestSchema, req)); + } + + public lockUser(userId: string): Promise { + return this.grpcService.userNew.lockUser(create(LockUserRequestSchema, { userId })); + } + + public unlockUser(userId: string): Promise { + return this.grpcService.userNew.unlockUser(create(UnlockUserRequestSchema, { userId })); + } + + public listAuthenticationFactors( + req: MessageInitShape, + ): Promise { + return this.grpcService.userNew.listAuthenticationFactors(create(ListAuthenticationFactorsRequestSchema, req)); + } + + public listPasskeys(req: MessageInitShape): Promise { + return this.grpcService.userNew.listPasskeys(create(ListPasskeysRequestSchema, req)); + } + + public removePasskeys(req: MessageInitShape): Promise { + return this.grpcService.userNew.removePasskey(create(RemovePasskeyRequestSchema, req)); + } + + public createPasskeyRegistrationLink( + req: MessageInitShape, + ): Promise { + return this.grpcService.userNew.createPasskeyRegistrationLink(create(CreatePasskeyRegistrationLinkRequestSchema, req)); + } + + public removePhone(userId: string): Promise { + return this.grpcService.userNew.removePhone(create(RemovePhoneRequestSchema, { userId })); + } + + public setPhone(req: MessageInitShape): Promise { + return this.grpcService.userNew.setPhone(create(SetPhoneRequestSchema, req)); + } + + public setEmail(req: MessageInitShape): Promise { + return this.grpcService.userNew.setEmail(create(SetEmailRequestSchema, req)); + } + + public removeTOTP(userId: string): Promise { + return this.grpcService.userNew.removeTOTP(create(RemoveTOTPRequestSchema, { userId })); + } + + public removeU2F(userId: string, u2fId: string): Promise { + return this.grpcService.userNew.removeU2F(create(RemoveU2FRequestSchema, { userId, u2fId })); + } + + public removeOTPSMS(userId: string): Promise { + return this.grpcService.userNew.removeOTPSMS(create(RemoveOTPSMSRequestSchema, { userId })); + } + + public removeOTPEmail(userId: string): Promise { + return this.grpcService.userNew.removeOTPEmail(create(RemoveOTPEmailRequestSchema, { userId })); + } + + public resendInviteCode(userId: string): Promise { + return this.grpcService.userNew.resendInviteCode(create(ResendInviteCodeRequestSchema, { userId })); + } + + public createInviteCode(req: MessageInitShape): Promise { + return this.grpcService.userNew.createInviteCode(create(CreateInviteCodeRequestSchema, req)); + } + + public passwordReset(req: MessageInitShape) { + return this.grpcService.userNew.passwordReset(create(PasswordResetRequestSchema, req)); + } + + public setPassword(req: MessageInitShape): Promise { + return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req)); + } +} + +function userToV2(user: User): UserV2 { + const details = user.getDetails(); + return create(UserSchema, { + userId: user.getId(), + details: details && detailsToV2(details), + state: user.getState() as number as UserState, + username: user.getUserName(), + loginNames: user.getLoginNamesList(), + preferredLoginName: user.getPreferredLoginName(), + type: typeToV2(user), + }); +} + +function detailsToV2(details: ObjectDetails): Details { + const changeDate = details.getChangeDate(); + return create(DetailsSchema, { + sequence: BigInt(details.getSequence()), + changeDate: changeDate && timestampToV2(changeDate), + resourceOwner: details.getResourceOwner(), + }); +} + +function timestampToV2(timestamp: Timestamp): TimestampV2 { + return create(TimestampSchema, { + seconds: BigInt(timestamp.getSeconds()), + nanos: timestamp.getNanos(), + }); +} + +function typeToV2(user: User): UserV2['type'] { + const human = user.getHuman(); + if (human) { + return { case: 'human', value: humanToV2(user, human) }; + } + + const machine = user.getMachine(); + if (machine) { + return { case: 'machine', value: machineToV2(machine) }; + } + + return { case: undefined }; +} + +function humanToV2(user: User, human: Human): HumanUser { + const profile = human.getProfile(); + const email = human.getEmail()?.getEmail(); + const phone = human.getPhone(); + const passwordChanged = human.getPasswordChanged(); + + return create(HumanUserSchema, { + userId: user.getId(), + state: user.getState() as number as UserState, + username: user.getUserName(), + loginNames: user.getLoginNamesList(), + preferredLoginName: user.getPreferredLoginName(), + profile: profile && humanProfileToV2(profile), + email: { email }, + phone: phone && humanPhoneToV2(phone), + passwordChangeRequired: false, + passwordChanged: passwordChanged && timestampToV2(passwordChanged), + }); +} + +function humanProfileToV2(profile: Profile): HumanProfile { + return create(HumanProfileSchema, { + givenName: profile.getFirstName(), + familyName: profile.getLastName(), + nickName: profile.getNickName(), + displayName: profile.getDisplayName(), + preferredLanguage: profile.getPreferredLanguage(), + gender: profile.getGender() as number as Gender, + avatarUrl: profile.getAvatarUrl(), + }); +} + +function humanPhoneToV2(phone: Phone): HumanPhone { + return create(HumanPhoneSchema, { + phone: phone.getPhone(), + isVerified: phone.getIsPhoneVerified(), + }); +} + +function machineToV2(machine: Machine): MachineUser { + return create(MachineUserSchema, { + name: machine.getName(), + description: machine.getDescription(), + hasSecret: machine.getHasSecret(), + accessTokenType: machine.getAccessTokenType() as number as AccessTokenType, + }); +} diff --git a/console/src/app/services/webkeys.service.ts b/console/src/app/services/webkeys.service.ts new file mode 100644 index 0000000000..9a26be4712 --- /dev/null +++ b/console/src/app/services/webkeys.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { GrpcService } from './grpc.service'; +import type { MessageInitShape } from '@bufbuild/protobuf'; +import { + DeleteWebKeyResponse, + ListWebKeysResponse, + CreateWebKeyRequestSchema, + CreateWebKeyResponse, + ActivateWebKeyResponse, +} from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb'; + +@Injectable({ + providedIn: 'root', +}) +export class WebKeysService { + constructor(private readonly grpcService: GrpcService) {} + + public ListWebKeys(): Promise { + return this.grpcService.webKey.listWebKeys({}); + } + + public DeleteWebKey(id: string): Promise { + return this.grpcService.webKey.deleteWebKey({ id }); + } + + public CreateWebKey(req: MessageInitShape): Promise { + return this.grpcService.webKey.createWebKey(req); + } + + public ActivateWebKey(id: string): Promise { + return this.grpcService.webKey.activateWebKey({ id }); + } +} diff --git a/console/src/app/utils/formatPhone.ts b/console/src/app/utils/formatPhone.ts index d5a246667c..a55adc23bf 100644 --- a/console/src/app/utils/formatPhone.ts +++ b/console/src/app/utils/formatPhone.ts @@ -1,6 +1,6 @@ import { CountryCode, parsePhoneNumber } from 'libphonenumber-js'; -export function formatPhone(phone: string): { phone: string; country: CountryCode } | null { +export function formatPhone(phone?: string): { phone: string; country: CountryCode } | null { const defaultCountry = 'US'; if (phone) { diff --git a/console/src/app/utils/framework.ts b/console/src/app/utils/framework.ts index bc26d77001..467cdb1341 100644 --- a/console/src/app/utils/framework.ts +++ b/console/src/app/utils/framework.ts @@ -1,6 +1,4 @@ -import { Framework } from '@netlify/framework-info/lib/types'; import { AddOIDCAppRequest } from '../proto/generated/zitadel/management_pb'; -import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames'; import { OIDCAppType, OIDCAuthMethodType, OIDCGrantType, OIDCResponseType } from '../proto/generated/zitadel/app_pb'; type OidcAppConfigurations = { diff --git a/console/src/app/utils/language.ts b/console/src/app/utils/language.ts index 4ef63dcb28..22eac99b3c 100644 --- a/console/src/app/utils/language.ts +++ b/console/src/app/utils/language.ts @@ -17,6 +17,7 @@ export const supportedLanguages = [ 'sv', 'hu', 'ko', + 'ro', ]; -export const supportedLanguagesRegexp: RegExp = /de|en|es|fr|id|it|ja|pl|zh|bg|pt|mk|cs|ru|nl|sv|hu|ko/; +export const supportedLanguagesRegexp: RegExp = /de|en|es|fr|id|it|ja|pl|zh|bg|pt|mk|cs|ru|nl|sv|hu|ko|ro/; export const fallbackLanguage: string = 'en'; diff --git a/console/src/app/utils/pairwiseStartWith.ts b/console/src/app/utils/pairwiseStartWith.ts new file mode 100644 index 0000000000..359394d14b --- /dev/null +++ b/console/src/app/utils/pairwiseStartWith.ts @@ -0,0 +1,11 @@ +import { Observable } from 'rxjs'; +import { map, pairwise, startWith } from 'rxjs/operators'; + +export function pairwiseStartWith(start: T) { + return (source: Observable) => + source.pipe( + startWith(start), + pairwise(), + map(([prev, curr]) => [prev, curr] as [T | R, R]), + ); +} diff --git a/console/src/app/utils/withLatestFromSynchronousFix.ts b/console/src/app/utils/withLatestFromSynchronousFix.ts new file mode 100644 index 0000000000..d2e362731e --- /dev/null +++ b/console/src/app/utils/withLatestFromSynchronousFix.ts @@ -0,0 +1,19 @@ +import { combineLatestWith, distinctUntilChanged, Observable, ObservableInput, ObservableInputTuple } from 'rxjs'; +import { map } from 'rxjs/operators'; + +// withLatestFrom does not work in this case, so we use +// combineLatestWith + distinctUntilChanged +// here the problem is described in more detail +// https://github.com/ReactiveX/rxjs/issues/7068 +export const withLatestFromSynchronousFix = + (...secondaries$: [...ObservableInputTuple]) => + (primary$: Observable) => + primary$.pipe( + // we add the index, so we can distinguish + // primary submissions from each other, + // and then we can only emit when primary changes + map((primary, i) => [primary, i]), + combineLatestWith(...secondaries$), + distinctUntilChanged(undefined!, ([[_, i]]) => i), + map(([[primary], ...secondaries]) => [primary, ...secondaries]), + ); diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 7c39fb22b2..4bd94d5ce6 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Животът на неактивния refresh токен е максималното време, през което refresh токен може да не се използва." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управлявайте вашите OIDC уеб ключове, за да подписвате и валидирате токени сигурно за вашата ZITADEL инстанция.", + "TABLE": { + "TITLE": "Активни и бъдещи уеб ключове", + "DESCRIPTION": "Вашите активни и предстоящи уеб ключове. Активирането на нов ключ ще деактивира текущия.", + "NOTE": "Забележка: Крайна точка JWKs OIDC връща кешируем отговор (по подразбиране 5 минути). Избягвайте активирането на ключ твърде рано, тъй като той може да не е наличен в кеша и клиентите.", + "ACTIVATE": "Активирайте следващия уеб ключ", + "ACTIVE": "В момента активен", + "NEXT": "Следващ в опашката", + "FUTURE": "Бъдещ", + "WARNING": "Уеб ключът е на по-малко от 5 минути" + }, + "CREATE": { + "TITLE": "Създаване на нов уеб ключ", + "DESCRIPTION": "Създаването на нов уеб ключ го добавя към вашия списък. ZITADEL използва ключове RSA2048 с хеш SHA256 по подразбиране.", + "KEY_TYPE": "Тип ключ", + "BITS": "Битове", + "HASHER": "Хешер", + "CURVE": "Крива" + }, + "PREVIOUS_TABLE": { + "TITLE": "Предишни уеб ключове", + "DESCRIPTION": "Това са вашите предишни уеб ключове, които вече не са активни.", + "DEACTIVATED_ON": "Деактивиран на" + } + }, "MESSAGE_TEXTS": { "TITLE": "Текстове на съобщенията", "DESCRIPTION": "Персонализирайте текстовете на вашите имейл или SMS уведомления. Ако искате да деактивирате някои от езиците, ограничете ги в настройките за език на вашите инстанции.", @@ -501,6 +527,115 @@ "DOWNLOAD": "Изтегляне", "APPLY": "Прилагам" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Действия", + "DESCRIPTION": "Действията ви позволяват да изпълнявате персонализиран код в отговор на API заявки, събития или специфични функции. Използвайте ги, за да разширите Zitadel, да автоматизирате работни процеси и да се интегрирате с други системи.", + "TYPES": { + "request": "Заявка", + "response": "Отговор", + "events": "Събития", + "function": "Функция" + }, + "DIALOG": { + "CREATE_TITLE": "Създаване на действие", + "UPDATE_TITLE": "Актуализиране на действие", + "TYPE": { + "DESCRIPTION": "Изберете кога искате да се изпълни това действие", + "REQUEST": { + "TITLE": "Заявка", + "DESCRIPTION": "Заявки, които се появяват в Zitadel. Това може да бъде нещо като заявка за вход." + }, + "RESPONSE": { + "TITLE": "Отговор", + "DESCRIPTION": "Отговор от заявка в Zitadel. Помислете за отговора, който получавате при извличане на потребител." + }, + "EVENTS": { + "TITLE": "Събития", + "DESCRIPTION": "Събития, които се случват в Zitadel. Това може да бъде нещо като създаване на потребителски акаунт, успешно влизане и т.н." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции, които можете да извикате в Zitadel. Това може да бъде всичко от изпращане на имейл до създаване на потребител." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Изберете дали това действие се прилага към всички заявки, конкретна услуга (напр. управление на потребители) или единична заявка (напр. създаване на потребител).", + "ALL": { + "TITLE": "Всички", + "DESCRIPTION": "Изберете това, ако искате да изпълните действието си при всяка заявка" + }, + "SELECT_SERVICE": { + "TITLE": "Избор на услуга", + "DESCRIPTION": "Изберете услуга на Zitadel за вашето действие." + }, + "SELECT_METHOD": { + "TITLE": "Избор на метод", + "DESCRIPTION": "Ако искате да изпълните само при конкретна заявка, изберете я тук", + "NOTE": "Ако не изберете метод, действието ви ще се изпълни при всяка заявка във вашата избрана услуга." + }, + "FUNCTIONNAME": { + "TITLE": "Име на функция", + "DESCRIPTION": "Изберете функцията, която искате да изпълните" + }, + "SELECT_GROUP": { + "TITLE": "Задаване на група", + "DESCRIPTION": "Ако искате да изпълните само върху група събития, задайте групата тук" + }, + "SELECT_EVENT": { + "TITLE": "Избор на събитие", + "DESCRIPTION": "Ако искате да изпълните само при конкретно събитие, посочете го тук" + } + }, + "TARGET": { + "DESCRIPTION": "Можете да изберете да изпълните цел или да я изпълните при същите условия като други цели.", + "TARGET": { + "DESCRIPTION": "Целта, която искате да изпълните за това действие" + }, + "CONDITIONS": { + "DESCRIPTION": "Условия за изпълнение" + } + } + }, + "TABLE": { + "CONDITION": "Условие", + "TYPE": "Тип", + "TARGET": "Цел", + "CREATIONDATE": "Дата на създаване" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Целта е дестинацията на кода, който искате да изпълните от действие. Създайте цел тук и я добавете към вашите действия.", + "CREATE": { + "TITLE": "Създаване на вашата цел", + "DESCRIPTION": "Създайте своя собствена цел извън Zitadel", + "NAME": "Име", + "NAME_DESCRIPTION": "Дайте на целта си ясно, описателно име, за да я идентифицирате лесно по-късно", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST уеб кука", + "restCall": "REST извикване", + "restAsync": "REST асинхронно" + }, + "ENDPOINT": "Крайна точка", + "ENDPOINT_DESCRIPTION": "Въведете крайната точка, където се хоства вашият код. Уверете се, че е достъпна за нас!", + "TIMEOUT": "Време за изчакване", + "TIMEOUT_DESCRIPTION": "Задайте максималното време, за което вашата цел трябва да отговори. Ако отнеме повече време, ще спрем заявката.", + "INTERRUPT_ON_ERROR": "Прекъсване при грешка", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Спрете всички изпълнения, когато целите върнат грешка", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: „Прекъсване при грешка“ спира операциите при неуспех, което може да доведе до блокиране. Тествайте с изключена опция, за да предотвратите блокиране на входа/създаването.", + "AWAIT_RESPONSE": "Изчакване на отговор", + "AWAIT_RESPONSE_DESCRIPTION": "Ще изчакаме отговор, преди да направим нещо друго. Полезно, ако възнамерявате да използвате множество цели за едно действие" + }, + "TABLE": { + "NAME": "Име", + "ENDPOINT": "Крайна точка", + "CREATIONDATE": "Дата на създаване", + "REORDER": "Преоразмеряване" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Има контрол върху цялата инстанция, включително всички организации", "IAM_OWNER_VIEWER": "Има разрешение да прегледа целия екземпляр, включително всички организации", @@ -508,14 +643,17 @@ "IAM_USER_MANAGER": "Има разрешение за създаване и управление на потребители", "IAM_ADMIN_IMPERSONATOR": "Има разрешение да се представя за администратор и крайни потребители от всички организации", "IAM_END_USER_IMPERSONATOR": "Има разрешение да се представя за крайни потребители от всички организации", + "IAM_LOGIN_CLIENT": "Има разрешение за управление на клиенти за вход", "ORG_OWNER": "Има разрешение за цялата организация", "ORG_USER_MANAGER": "Има разрешение да създава и управлява потребители на организацията", "ORG_OWNER_VIEWER": "Има разрешение за преглед на цялата организация", + "ORG_SETTINGS_MANAGER": "Има разрешение за управление на настройките на организацията", "ORG_USER_PERMISSION_EDITOR": "Има разрешение за управление на потребителски безвъзмездни средства", "ORG_PROJECT_PERMISSION_EDITOR": "Има разрешение за управление на грантове по проекти", "ORG_PROJECT_CREATOR": "Има разрешение да създава свои собствени проекти и основни настройки", "ORG_ADMIN_IMPERSONATOR": "Има разрешение да се представя за администратор и крайни потребители от организацията", "ORG_END_USER_IMPERSONATOR": "Има разрешение да се представя за крайни потребители от организацията", + "ORG_USER_SELF_MANAGER": "Има разрешение да управлява собствените си потребители", "PROJECT_OWNER": "Има разрешение върху целия проект", "PROJECT_OWNER_VIEWER": "Има разрешение за преглед на целия проект", "PROJECT_OWNER_GLOBAL": "Има разрешение върху целия проект", @@ -786,7 +924,10 @@ "PHONESECTION": "Телефонни номера", "PASSWORDSECTION": "Първоначална парола", "ADDRESSANDPHONESECTION": "Телефонен номер", - "INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. " + "INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. ", + "SETUPAUTHENTICATIONLATER": "Настройте удостоверяване по-късно за този потребител.", + "INVITATION": "Изпратете покана по имейл за настройка на удостоверяване и потвърждение на имейл.", + "INITIALPASSWORD": "Задайте начална парола за потребителя." }, "CODEDIALOG": { "TITLE": "Потвърдете телефонния номер", @@ -808,6 +949,9 @@ "EMAIL": "Електронна поща", "PHONE": "Телефонен номер", "PHONE_HINT": "Използвайте символа +, последван от кода на държавата, на която се обаждате, или изберете държавата от падащото меню и накрая въведете телефонния номер", + "PHONE_VERIFIED": "Телефонният номер е потвърден", + "SEND_SMS": "Изпрати SMS за потвърждение", + "SEND_EMAIL": "Изпрати имейл", "USERNAME": "Потребителско име", "CHANGEUSERNAME": "променям", "CHANGEUSERNAME_TITLE": "Промяна на потребителското име", @@ -948,6 +1092,14 @@ "5": "Спряно", "6": "Първоначално" }, + "STATEV2": { + "0": "неизвестен", + "1": "Активен", + "2": "Неактивен", + "3": "Изтрито", + "4": "Заключено", + "5": "Първоначално" + }, "SEARCH": { "ADDITIONAL": "Име за вход (текуща организация)", "ADDITIONAL-EXTERNAL": "Име за вход (външна организация)" @@ -1339,6 +1491,7 @@ "BRANDING": "Брандиране", "PRIVACYPOLICY": "Политика за бедност", "OIDC": "Живот и изтичане на OIDC Token", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Тайна поява", "SECURITY": "Настройки на сигурността", "EVENTS": "Събития", @@ -1384,7 +1537,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1475,6 +1629,16 @@ "ACTIONS_DESCRIPTION": "Действия v2 позволяват управление на выполнения на данни и цели. Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Завършване на сесия", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Ако флагът е активиран, ще можете да прекратите единична сесия от UI за вход, като предоставите id_token с `sid` претенция като id_token_hint на крайната точка на end_session. Имайте предвид, че в момента всички сесии от същия потребителски агент (браузър) се прекратяват в UI за вход. Сесиите, управлявани чрез API на сесията, вече позволяват прекратяването на единични сесии.", + "DEBUGOIDCPARENTERROR": "Отстраняване на грешки на OIDC родителя", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Ако флагът е активиран, грешката на OIDC родителя ще бъде записана в конзолата.", + "DISABLEUSERTOKENEVENT": "Деактивиране на събитие за потребителски токен", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Активиране на Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout имплементира OpenID Connect Back-Channel Logout 1.0 и може да се използва за уведомяване на клиентите за прекратяване на сесията при OpenID доставчика.", + "PERMISSIONCHECKV2": "Проверка на разрешения V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", + "WEBKEY": "Уеб ключ", + "WEBKEY_DESCRIPTION": "Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", "STATES": { "INHERITED": "Наследено", "ENABLED": "Активирано", @@ -1485,7 +1649,12 @@ "ENABLED": "„Активирано“ се наследява", "DISABLED": "„Деактивирано“ се наследява" }, - "RESET": "Задай всички на наследено" + "RESET": "Задай всички на наследено", + "CONSOLEUSEV2USERAPI": "Използвайте V2 API в конзолата за създаване на потребител", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Когато този флаг е активиран, конзолата използва V2 User API за създаване на нови потребители. С V2 API новосъздадените потребители започват без начален статус.", + "LOGINV2": "Вход V2", + "LOGINV2_DESCRIPTION": "Активирането на това включва новия потребителски интерфейс за вход, базиран на TypeScript, с подобрена сигурност, производителност и възможности за персонализиране.", + "LOGINV2_BASEURI": "Базов URI" }, "DIALOG": { "RESET": { @@ -1622,7 +1791,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Проверката на имейл е извършена", @@ -2187,7 +2358,9 @@ "REMOVED": "Премахнато успешно." }, "ISIDTOKENMAPPING": "Съответствие от ID токен", - "ISIDTOKENMAPPING_DESC": "Ако е избрано, информацията на доставчика се съответства от ID токена, а не от userinfo крайната точка." + "ISIDTOKENMAPPING_DESC": "Ако е избрано, информацията на доставчика се съответства от ID токена, а не от userinfo крайната точка.", + "USEPKCE": "Използвайте PKCE", + "USEPKCE_DESC": "Определя дали параметрите code_challenge и code_challenge_method са включени в заявката за удостоверяване" }, "MFA": { "LIST": { @@ -2566,7 +2739,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Добавяне на мениджър", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 09b9ac2b9f..6dd066789e 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Životnost nečinného refresh tokenu je maximální doba, po kterou může být refresh token nepoužitý." } }, + "WEB_KEYS": { + "DESCRIPTION": "Spravujte své OIDC webové klíče pro bezpečné podepisování a ověřování tokenů ve vaší instanci ZITADEL.", + "TABLE": { + "TITLE": "Aktivní a budoucí webové klíče", + "DESCRIPTION": "Vaše aktivní a nadcházející webové klíče. Aktivací nového klíče dojde k deaktivaci aktuálního.", + "NOTE": "Poznámka: Koncový bod JWKs OIDC vrací odpověď uložitelnou do mezipaměti (výchozí 5 minut). Vyhněte se příliš brzké aktivaci klíče, protože nemusí být dostupný v mezipaměti a klientům.", + "ACTIVATE": "Aktivovat další webový klíč", + "ACTIVE": "Aktuálně aktivní", + "NEXT": "Další v řadě", + "FUTURE": "Budoucí", + "WARNING": "Webový klíč je starý méně než 5 minut" + }, + "CREATE": { + "TITLE": "Vytvořit nový webový klíč", + "DESCRIPTION": "Vytvořením nového webového klíče jej přidáte do svého seznamu. ZITADEL používá klíče RSA2048 s hashováním SHA256 jako výchozí.", + "KEY_TYPE": "Typ klíče", + "BITS": "Bity", + "HASHER": "Hasher", + "CURVE": "Křivka" + }, + "PREVIOUS_TABLE": { + "TITLE": "Předchozí webové klíče", + "DESCRIPTION": "Toto jsou vaše předchozí webové klíče, které již nejsou aktivní.", + "DEACTIVATED_ON": "Deaktivováno dne" + } + }, "MESSAGE_TEXTS": { "TITLE": "Texty zpráv", "DESCRIPTION": "Přizpůsob si texty svých e-mailových nebo SMS notifikací. Pokud chceš některé z jazyků zakázat, omez je ve svém nastavení jazyků instance.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Stáhnout", "APPLY": "Platit" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Akce", + "DESCRIPTION": "Akce vám umožňují spouštět vlastní kód v reakci na požadavky API, události nebo specifické funkce. Použijte je k rozšíření Zitadel, automatizaci pracovních postupů a integraci s dalšími systémy.", + "TYPES": { + "request": "Požadavek", + "response": "Odpověď", + "events": "Události", + "function": "Funkce" + }, + "DIALOG": { + "CREATE_TITLE": "Vytvořit akci", + "UPDATE_TITLE": "Aktualizovat akci", + "TYPE": { + "DESCRIPTION": "Vyberte, kdy chcete tuto akci spustit", + "REQUEST": { + "TITLE": "Požadavek", + "DESCRIPTION": "Požadavky, které se vyskytují v rámci Zitadel. Může to být něco jako volání požadavku na přihlášení." + }, + "RESPONSE": { + "TITLE": "Odpověď", + "DESCRIPTION": "Odpověď na požadavek v rámci Zitadel. Představte si odpověď, kterou získáte při načítání uživatele." + }, + "EVENTS": { + "TITLE": "Události", + "DESCRIPTION": "Události, které se dějí v rámci Zitadel. Může to být cokoli, jako je vytvoření uživatelského účtu, úspěšné přihlášení atd." + }, + "FUNCTIONS": { + "TITLE": "Funkce", + "DESCRIPTION": "Funkce, které můžete volat v rámci Zitadel. Může to být cokoli od odeslání e-mailu po vytvoření uživatele." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Vyberte, zda se tato akce vztahuje na všechny požadavky, konkrétní službu (např. správa uživatelů) nebo jeden požadavek (např. vytvoření uživatele).", + "ALL": { + "TITLE": "Všechny", + "DESCRIPTION": "Vyberte tuto možnost, pokud chcete spustit akci pro každý požadavek" + }, + "SELECT_SERVICE": { + "TITLE": "Vybrat službu", + "DESCRIPTION": "Vyberte službu Zitadel pro svou akci." + }, + "SELECT_METHOD": { + "TITLE": "Vybrat metodu", + "DESCRIPTION": "Pokud chcete spustit pouze pro konkrétní požadavek, vyberte jej zde", + "NOTE": "Pokud nevyberete metodu, vaše akce se spustí pro každý požadavek ve vaší vybrané službě." + }, + "FUNCTIONNAME": { + "TITLE": "Název funkce", + "DESCRIPTION": "Vyberte funkci, kterou chcete spustit" + }, + "SELECT_GROUP": { + "TITLE": "Nastavit skupinu", + "DESCRIPTION": "Pokud chcete spustit pouze pro skupinu událostí, nastavte zde skupinu" + }, + "SELECT_EVENT": { + "TITLE": "Vybrat událost", + "DESCRIPTION": "Pokud chcete spustit pouze pro konkrétní událost, zadejte ji zde" + } + }, + "TARGET": { + "DESCRIPTION": "Můžete se rozhodnout spustit cíl nebo jej spustit za stejných podmínek jako jiné cíle.", + "TARGET": { + "DESCRIPTION": "Cíl, který chcete spustit pro tuto akci" + }, + "CONDITIONS": { + "DESCRIPTION": "Podmínky spuštění" + } + } + }, + "TABLE": { + "CONDITION": "Podmínka", + "TYPE": "Typ", + "TARGET": "Cíl", + "CREATIONDATE": "Datum vytvoření" + } + }, + "TARGET": { + "TITLE": "Cíle", + "DESCRIPTION": "Cíl je cíl kódu, který chcete spustit z akce. Vytvořte zde cíl a přidejte jej do svých akcí.", + "CREATE": { + "TITLE": "Vytvořit cíl", + "DESCRIPTION": "Vytvořte si vlastní cíl mimo Zitadel", + "NAME": "Název", + "NAME_DESCRIPTION": "Dejte svému cíli jasný, popisný název, aby bylo snadné jej později identifikovat", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Volání", + "restAsync": "REST Asynchronní" + }, + "ENDPOINT": "Koncový bod", + "ENDPOINT_DESCRIPTION": "Zadejte koncový bod, kde je hostován váš kód. Ujistěte se, že je pro nás přístupný!", + "TIMEOUT": "Časový limit", + "TIMEOUT_DESCRIPTION": "Nastavte maximální dobu, po kterou musí váš cíl odpovědět. Pokud to trvá déle, požadavek zastavíme.", + "INTERRUPT_ON_ERROR": "Přerušit při chybě", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Zastavte všechna spuštění, když cíle vrátí chybu", + "INTERRUPT_ON_ERROR_WARNING": "Pozor: „Přerušit při chybě“ zastaví operace při selhání, což může vést k zablokování. Otestujte s vypnutou možností, abyste předešli zablokování přihlášení/vytváření.", + "AWAIT_RESPONSE": "Čekat na odpověď", + "AWAIT_RESPONSE_DESCRIPTION": "Před provedením čehokoli jiného počkáme na odpověď. Užitečné, pokud hodláte použít více cílů pro jednu akci" + }, + "TABLE": { + "NAME": "Název", + "ENDPOINT": "Koncový bod", + "CREATIONDATE": "Datum vytvoření", + "REORDER": "Změnit pořadí" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Má kontrolu nad celou instancí, včetně všech organizací", "IAM_OWNER_VIEWER": "Má oprávnění prohlížet celou instanci, včetně všech organizací", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Má oprávnění vytvářet a spravovat uživatele", "IAM_ADMIN_IMPERSONATOR": "Má oprávnění vydávat se za správce a koncové uživatele ze všech organizací", "IAM_END_USER_IMPERSONATOR": "Má oprávnění vydávat se za koncové uživatele ze všech organizací", + "IAM_LOGIN_CLIENT": "Má oprávnění spravovat přihlašovací klienty", "ORG_OWNER": "Má oprávnění nad celou organizací", "ORG_USER_MANAGER": "Má oprávnění vytvářet a spravovat uživatele organizace", "ORG_OWNER_VIEWER": "Má oprávnění prohlížet celou organizaci", + "ORG_SETTINGS_MANAGER": "Má oprávnění spravovat nastavení organizace", "ORG_USER_PERMISSION_EDITOR": "Má oprávnění spravovat uživatelská pověření", "ORG_PROJECT_PERMISSION_EDITOR": "Má oprávnění spravovat pověření projektu", "ORG_PROJECT_CREATOR": "Má oprávnění vytvářet své vlastní projekty a podřízená nastavení", "ORG_ADMIN_IMPERSONATOR": "Má oprávnění vydávat se za správce a koncové uživatele z organizace", "ORG_END_USER_IMPERSONATOR": "Má oprávnění vydávat se za koncové uživatele z organizace", + "ORG_USER_SELF_MANAGER": "Má oprávnění spravovat svůj vlastní uživatelský účet", "PROJECT_OWNER": "Má oprávnění nad celým projektem", "PROJECT_OWNER_VIEWER": "Má oprávnění prohlížet celý projekt", "PROJECT_OWNER_GLOBAL": "Má oprávnění nad celým projektem", @@ -787,7 +925,10 @@ "PHONESECTION": "Telefonní čísla", "PASSWORDSECTION": "Prvotní heslo", "ADDRESSANDPHONESECTION": "Telefonní číslo", - "INITMAILDESCRIPTION": "Pokud jsou vybrány obě možnosti, nebude odeslán e-mail pro inicializaci. Pokud je vybrána pouze jedna z možností, bude odeslán e-mail pro poskytnutí/ověření údajů." + "INITMAILDESCRIPTION": "Pokud jsou vybrány obě možnosti, nebude odeslán e-mail pro inicializaci. Pokud je vybrána pouze jedna z možností, bude odeslán e-mail pro poskytnutí/ověření údajů.", + "SETUPAUTHENTICATIONLATER": "Nastavte ověřování později pro tohoto uživatele.", + "INVITATION": "Odešlete pozvánkový e-mail pro nastavení ověřování a ověření e-mailu.", + "INITIALPASSWORD": "Nastavte počáteční heslo pro uživatele." }, "CODEDIALOG": { "TITLE": "Ověření telefonního čísla", @@ -809,6 +950,9 @@ "EMAIL": "E-mail", "PHONE": "Telefonní číslo", "PHONE_HINT": "Použijte symbol + následovaný mezinárodním kódem země, nebo ze seznamu vyberte zemi a nakonec zadejte telefonní číslo", + "PHONE_VERIFIED": "Telefonní číslo ověřeno", + "SEND_SMS": "Odeslat ověřovací SMS", + "SEND_EMAIL": "Odeslat E-mail", "USERNAME": "Uživatelské jméno", "CHANGEUSERNAME": "upravit", "CHANGEUSERNAME_TITLE": "Změna uživatelského jména", @@ -949,6 +1093,14 @@ "5": "Pozastavený", "6": "Počáteční" }, + "STATEV2": { + "0": "Neznámý", + "1": "Aktivní", + "2": "Neaktivní", + "3": "Smazaný", + "4": "Uzamčený", + "5": "Počáteční" + }, "SEARCH": { "ADDITIONAL": "Přihlašovací jméno (současná organizace)", "ADDITIONAL-EXTERNAL": "Přihlašovací jméno (externí organizace)" @@ -1340,6 +1492,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Zásady ochrany osobních údajů", "OIDC": "Životnost a expirace OIDC tokenu", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Generátor tajemství", "SECURITY": "Bezpečnostní nastavení", "EVENTS": "Události", @@ -1385,7 +1538,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1476,6 +1630,16 @@ "ACTIONS_DESCRIPTION": "Akce v2 umožňují správu datových provedení a cílů. Pokud je tento příznak povolen, budete moci používat nové API a jeho funkce.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 ukončení relace", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Pokud je příznak aktivován, budete moci ukončit jedinou relaci z rozhraní pro přihlášení zadáním id_token s nárokem `sid` jako id_token_hint na koncovém bodu end_session. Poznamenejte si, že v současné době jsou v rozhraní pro přihlášení ukončeny všechny relace ze stejného uživatelského agenta (prohlížeče). Relace spravované prostřednictvím rozhraní API relace již umožňují ukončení jednotlivých relací.", + "DEBUGOIDCPARENTERROR": "Debugování chyby rodiče OIDC", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Pokud je příznak povolen, chyba rodiče OIDC bude zaznamenána v konzoli.", + "DISABLEUSERTOKENEVENT": "Zakázat událost uživatelského tokenu", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Povolit Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementuje OpenID Connect Back-Channel Logout 1.0 a může být použit k informování klientů o ukončení relace u poskytovatele OpenID.", + "PERMISSIONCHECKV2": "Kontrola oprávnění V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Pokud je příznak povolen, budete moci používat nový API a jeho funkce.", + "WEBKEY": "Webový klíč", + "WEBKEY_DESCRIPTION": "Pokud je příznak povolen, budete moci používat nový API a jeho funkce.", "STATES": { "INHERITED": "Děděno", "ENABLED": "Povoleno", @@ -1486,7 +1650,12 @@ "ENABLED": "„Povoleno“ je zděděno", "DISABLED": "„Zakázáno“ je zděděno" }, - "RESET": "Nastavit vše na děděné" + "RESET": "Nastavit vše na děděné", + "CONSOLEUSEV2USERAPI": "Použijte V2 API v konzoli pro vytvoření uživatele", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Když je tato příznak povolen, konzole používá V2 User API k vytvoření nových uživatelů. S V2 API nově vytvoření uživatelé začínají bez počátečního stavu.", + "LOGINV2": "Přihlášení V2", + "LOGINV2_DESCRIPTION": "Povolením této možnosti se aktivuje nové přihlašovací rozhraní založené na TypeScriptu s vylepšeným zabezpečením, výkonem a přizpůsobitelností.", + "LOGINV2_BASEURI": "Základní URI" }, "DIALOG": { "RESET": { @@ -1623,7 +1792,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Ověření e-mailu dokončeno", @@ -2200,7 +2371,9 @@ "REMOVED": "Úspěšně odebráno." }, "ISIDTOKENMAPPING": "Mapování z ID tokenu", - "ISIDTOKENMAPPING_DESC": "Pokud je vybráno, informace o poskytovateli jsou mapovány z ID tokenu, nikoli z koncového bodu userinfo." + "ISIDTOKENMAPPING_DESC": "Pokud je vybráno, informace o poskytovateli jsou mapovány z ID tokenu, nikoli z koncového bodu userinfo.", + "USEPKCE": "Použijte PKCE", + "USEPKCE_DESC": "Určuje, zda jsou v požadavku na ověření zahrnuty parametry code_challenge a code_challenge_method" }, "MFA": { "LIST": { @@ -2579,7 +2752,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Přidat manažera", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 1a9efe8c97..cb973006e4 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Die maximale Inaktivitätsdauer eines Aktualisierungstokens ist die maximale Zeit, in der ein Aktualisierungstoken unbenutzt sein kann." } }, + "WEB_KEYS": { + "DESCRIPTION": "Verwalte deine OIDC Web Keys, um Tokens für deine ZITADEL-Instanz sicher zu signieren und zu validieren.", + "TABLE": { + "TITLE": "Aktive und zukünftige Web Keys", + "DESCRIPTION": "Deine aktiven und kommenden Web Keys. Das Aktivieren eines neuen Schlüssels deaktiviert den aktuellen.", + "NOTE": "Hinweis: Der JWKs OIDC-Endpunkt gibt eine zwischenspeicherbare Antwort zurück (Standard: 5 Min.). Vermeide es, einen Schlüssel zu früh zu aktivieren, da er möglicherweise noch nicht in Caches und Clients verfügbar ist.", + "ACTIVATE": "Nächsten Web Key aktivieren", + "ACTIVE": "Derzeit aktiv", + "NEXT": "Als Nächstes in der Warteschlange", + "FUTURE": "Zukünftig", + "WARNING": "Der Web Key ist weniger als 5 Minuten alt" + }, + "CREATE": { + "TITLE": "Neuen Web Key erstellen", + "DESCRIPTION": "Das Erstellen eines neuen Web Keys fügt ihn zu deiner Liste hinzu. ZITADEL verwendet standardmäßig RSA2048-Schlüssel mit einem SHA256-Hasher.", + "KEY_TYPE": "Schlüsseltyp", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Kurve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Frühere Web Keys", + "DESCRIPTION": "Dies sind deine früheren Web Keys, die nicht mehr aktiv sind.", + "DEACTIVATED_ON": "Deaktiviert am" + } + }, "MESSAGE_TEXTS": { "TITLE": "Nachrichtentexte", "DESCRIPTION": "Passe die Texte deiner Benachrichtigungs-E-Mails oder SMS-Nachrichten an. Wenn du einige der Sprachen deaktivieren möchtest, beschränke sie in den Spracheinstellungen deiner Instanz.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Herunterladen", "APPLY": "Anwenden" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Aktionen", + "DESCRIPTION": "Aktionen ermöglichen es Ihnen, benutzerdefinierten Code als Reaktion auf API-Anfragen, Ereignisse oder bestimmte Funktionen auszuführen. Verwenden Sie sie, um Zitadel zu erweitern, Arbeitsabläufe zu automatisieren und sich in andere Systeme zu integrieren.", + "TYPES": { + "request": "Anfrage", + "response": "Antwort", + "events": "Ereignisse", + "function": "Funktion" + }, + "DIALOG": { + "CREATE_TITLE": "Eine Aktion erstellen", + "UPDATE_TITLE": "Eine Aktion aktualisieren", + "TYPE": { + "DESCRIPTION": "Wählen Sie aus, wann diese Aktion ausgeführt werden soll", + "REQUEST": { + "TITLE": "Anfrage", + "DESCRIPTION": "Anfragen, die innerhalb von Zitadel auftreten. Dies könnte so etwas wie ein Login-Anfrageaufruf sein." + }, + "RESPONSE": { + "TITLE": "Antwort", + "DESCRIPTION": "Eine Antwort auf eine Anfrage innerhalb von Zitadel. Denken Sie an die Antwort, die Sie beim Abrufen eines Benutzers erhalten." + }, + "EVENTS": { + "TITLE": "Ereignisse", + "DESCRIPTION": "Ereignisse, die innerhalb von Zitadel stattfinden. Dies könnte alles sein, wie z.B. das Erstellen eines Benutzerkontos, ein erfolgreicher Login usw." + }, + "FUNCTIONS": { + "TITLE": "Funktionen", + "DESCRIPTION": "Funktionen, die Sie innerhalb von Zitadel aufrufen können. Dies könnte alles sein, vom Senden einer E-Mail bis zum Erstellen eines Benutzers." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Wählen Sie aus, ob diese Aktion für alle Anfragen, einen bestimmten Dienst (z.B. Benutzerverwaltung) oder eine einzelne Anfrage (z.B. Benutzer erstellen) gelten soll.", + "ALL": { + "TITLE": "Alle", + "DESCRIPTION": "Wählen Sie dies aus, wenn Sie Ihre Aktion bei jeder Anfrage ausführen möchten" + }, + "SELECT_SERVICE": { + "TITLE": "Dienst auswählen", + "DESCRIPTION": "Wählen Sie einen Zitadel-Dienst für Ihre Aktion aus." + }, + "SELECT_METHOD": { + "TITLE": "Methode auswählen", + "DESCRIPTION": "Wenn Sie nur bei einer bestimmten Anfrage ausführen möchten, wählen Sie sie hier aus", + "NOTE": "Wenn Sie keine Methode auswählen, wird Ihre Aktion bei jeder Anfrage in Ihrem ausgewählten Dienst ausgeführt." + }, + "FUNCTIONNAME": { + "TITLE": "Funktionsname", + "DESCRIPTION": "Wählen Sie die Funktion aus, die Sie ausführen möchten" + }, + "SELECT_GROUP": { + "TITLE": "Gruppe festlegen", + "DESCRIPTION": "Wenn Sie nur bei einer Gruppe von Ereignissen ausführen möchten, legen Sie die Gruppe hier fest" + }, + "SELECT_EVENT": { + "TITLE": "Ereignis auswählen", + "DESCRIPTION": "Wenn Sie nur bei einem bestimmten Ereignis ausführen möchten, geben Sie es hier an" + } + }, + "TARGET": { + "DESCRIPTION": "Sie können wählen, ob Sie ein Ziel ausführen oder es unter den gleichen Bedingungen wie andere Ziele ausführen möchten.", + "TARGET": { + "DESCRIPTION": "Das Ziel, das Sie für diese Aktion ausführen möchten" + }, + "CONDITIONS": { + "DESCRIPTION": "Ausführungsbedingungen" + } + } + }, + "TABLE": { + "CONDITION": "Bedingung", + "TYPE": "Typ", + "TARGET": "Ziel", + "CREATIONDATE": "Erstellungsdatum" + } + }, + "TARGET": { + "TITLE": "Ziele", + "DESCRIPTION": "Ein Ziel ist das Ziel des Codes, den Sie von einer Aktion ausführen möchten. Erstellen Sie hier ein Ziel und fügen Sie es Ihren Aktionen hinzu.", + "CREATE": { + "TITLE": "Ihr Ziel erstellen", + "DESCRIPTION": "Erstellen Sie Ihr eigenes Ziel außerhalb von Zitadel", + "NAME": "Name", + "NAME_DESCRIPTION": "Geben Sie Ihrem Ziel einen klaren, beschreibenden Namen, um es später leicht identifizieren zu können", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Aufruf", + "restAsync": "REST Asynchron" + }, + "ENDPOINT": "Endpunkt", + "ENDPOINT_DESCRIPTION": "Geben Sie den Endpunkt ein, an dem Ihr Code gehostet wird. Stellen Sie sicher, dass er für uns zugänglich ist!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Legen Sie die maximale Zeit fest, die Ihr Ziel zum Antworten hat. Wenn es länger dauert, stoppen wir die Anfrage.", + "INTERRUPT_ON_ERROR": "Bei Fehler unterbrechen", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stoppen Sie alle Ausführungen, wenn die Ziele einen Fehler zurückgeben", + "INTERRUPT_ON_ERROR_WARNING": "Achtung: „Bei Fehler unterbrechen“ stoppt Vorgänge bei einem Fehler und kann zur Sperrung führen. Testen Sie mit deaktivierter Option, um Login/Erstellung nicht zu blockieren.", + "AWAIT_RESPONSE": "Auf Antwort warten", + "AWAIT_RESPONSE_DESCRIPTION": "Wir warten auf eine Antwort, bevor wir etwas anderes tun. Nützlich, wenn Sie mehrere Ziele für eine einzelne Aktion verwenden möchten" + }, + "TABLE": { + "NAME": "Name", + "ENDPOINT": "Endpunkt", + "CREATIONDATE": "Erstellungsdatum", + "REORDER": "Verschieben" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Hat die Kontrolle über die gesamte Instanz, einschließlich aller Organisationen", "IAM_OWNER_VIEWER": "Hat die Leseberechtigung, die gesamte Instanz einschließlich aller Organisationen zu überprüfen", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Hat die Berechtigung zum Erstellen und Verwalten von Benutzern", "IAM_ADMIN_IMPERSONATOR": "Hat die Berechtigung, sich als Administrator und Endbenutzer aller Organisationen auszugeben", "IAM_END_USER_IMPERSONATOR": "Hat die Berechtigung, sich als Endbenutzer aller Organisationen auszugeben", + "IAM_LOGIN_CLIENT": "Hat die Berechtigung, Anmeldeclients zu verwalten", "ORG_OWNER": "Hat die Berechtigung für die gesamte Organisation", "ORG_USER_MANAGER": "Hat die Berechtigung, Benutzer der Organisation zu erstellen und zu verwalten", "ORG_OWNER_VIEWER": "Hat die Leseberechtigung, die gesamte Organisation zu überprüfen", + "ORG_SETTINGS_MANAGER": "Hat die Berechtigung, die Einstellungen der Organisation zu verwalten", "ORG_USER_PERMISSION_EDITOR": "Verfügt über die Berechtigung zum Verwalten von User grants", "ORG_PROJECT_PERMISSION_EDITOR": "Hat die Berechtigung, Projektberechtigungen für externe Organisationen zu verwalten", "ORG_PROJECT_CREATOR": "Hat die Berechtigung, seine eigenen Projekte und dessen Einstellungen zu erstellen", "ORG_ADMIN_IMPERSONATOR": "Hat die Berechtigung, sich als Administrator und Endbenutzer der Organisation auszugeben", "ORG_END_USER_IMPERSONATOR": "Hat die Berechtigung, sich als Endbenutzer der Organisation auszugeben", + "ORG_USER_SELF_MANAGER": "Hat die Berechtigung, seinen eigenen Benutzer zu verwalten", "PROJECT_OWNER": "Hat die Berechtigung für das gesamte Projekt", "PROJECT_OWNER_VIEWER": "Hat die Leseberechtigung, das gesamte Projekt zu überprüfen", "PROJECT_OWNER_GLOBAL": "Hat die Berechtigung für das gesamte Projekt", @@ -787,7 +925,10 @@ "PHONESECTION": "Telefonnummer", "PASSWORDSECTION": "Setze ein initiales Passwort.", "ADDRESSANDPHONESECTION": "Telefonnummer", - "INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Verifikation der Daten gesendet." + "INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Verifikation der Daten gesendet.", + "SETUPAUTHENTICATIONLATER": "Authentifizierung später für diesen Benutzer einrichten.", + "INVITATION": "Eine Einladung per E-Mail für die Authentifizierungseinrichtung und E-Mail-Verifizierung senden.", + "INITIALPASSWORD": "Setze ein initiales Passwort für den Benutzer." }, "CODEDIALOG": { "TITLE": "Telefonnummer verifizieren", @@ -809,6 +950,9 @@ "EMAIL": "E-Mail", "PHONE": "Telefonnummer", "PHONE_HINT": "Verwenden das Symbol + gefolgt von der Landesvorwahl des Anrufers oder wähle das Land aus der Dropdown-Liste aus und gebe anschließend die Telefonnummer ein.", + "PHONE_VERIFIED": "Telefonnummer verifiziert", + "SEND_SMS": "Bestätigungs-SMS senden", + "SEND_EMAIL": "E-Mail senden", "USERNAME": "Benutzername", "CHANGEUSERNAME": "bearbeiten", "CHANGEUSERNAME_TITLE": "Benutzername ändern", @@ -949,6 +1093,14 @@ "5": "Suspendiert", "6": "Initialisiert" }, + "STATEV2": { + "0": "Unbekannt", + "1": "Aktiv", + "2": "Inaktiv", + "3": "Gelöscht", + "4": "Gesperrt", + "5": "Initialisiert" + }, "SEARCH": { "ADDITIONAL": "Benutzer Name (eigene Organisation)", "ADDITIONAL-EXTERNAL": "Loginname (externe Organisation)" @@ -1340,6 +1492,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Datenschutzrichtlinie", "OIDC": "OIDC Token Lifetime und Expiration", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Secret Generator", "SECURITY": "Sicherheitseinstellungen", "EVENTS": "Events", @@ -1385,7 +1538,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1476,6 +1630,16 @@ "ACTIONS_DESCRIPTION": "Aktionen v2 ermöglichen die Verwaltung von Datenausführungen und Zielen. Wenn das Flag aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Sitzungsbeendigung", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Wenn das Flag aktiviert ist, können Sie eine einzelne Sitzung über die Login-Benutzeroberfläche beenden, indem Sie einen id_token mit einem `sid` Claim als id_token_hint am Endpunkt end_session übergeben. Beachten Sie, dass derzeit alle Sitzungen desselben Benutzeragenten (Browser) in der Login-Benutzeroberfläche beendet werden. Sitzungen, die über die Session API verwaltet werden, ermöglichen bereits die Beendigung einzelner Sitzungen.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Wenn die Flagge aktiviert ist, wird der OIDC-Elternfehler in der Konsole protokolliert.", + "DISABLEUSERTOKENEVENT": "Benutzer-Token-Ereignis deaktivieren", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Backchannel-Logout aktivieren", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Der Back-Channel-Logout implementiert OpenID Connect Back-Channel Logout 1.0 und kann verwendet werden, um Clients über die Beendigung der Sitzung beim OpenID-Provider zu benachrichtigen.", + "PERMISSIONCHECKV2": "Berechtigungsprüfung V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", + "WEBKEY": "Web-Schlüssel", + "WEBKEY_DESCRIPTION": "Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", "STATES": { "INHERITED": "Erben", "ENABLED": "Aktiviert", @@ -1486,7 +1650,12 @@ "ENABLED": "„Aktiviert“ wird vererbt", "DISABLED": "„Deaktiviert“ wird vererbt" }, - "RESET": "Alle auf Erben setzen" + "RESET": "Alle auf Erben setzen", + "CONSOLEUSEV2USERAPI": "Verwende die V2-API in der Konsole zur Erstellung von Benutzern", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Wenn diese Option aktiviert ist, verwendet die Konsole die V2 User API, um neue Benutzer zu erstellen. Mit der V2 API starten neu erstellte Benutzer nicht im Initial Zustand.", + "LOGINV2": "Login V2", + "LOGINV2_DESCRIPTION": "Durch das Aktivieren wird das neue TypeScript-basierte Login-UI mit verbesserter Sicherheit, Leistung und Anpassbarkeit aktiviert.", + "LOGINV2_BASEURI": "Basis-URI" }, "DIALOG": { "RESET": { @@ -1623,7 +1792,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Email Verification erfolgreich", @@ -2191,7 +2362,9 @@ "REMOVED": "Erfolgreich entfernt." }, "ISIDTOKENMAPPING": "Zuordnung vom ID-Token", - "ISIDTOKENMAPPING_DESC": "Legt fest, ob für das Mapping der Provider Informationen das ID-Token verwendet werden soll, anstatt des Userinfo-Endpoints." + "ISIDTOKENMAPPING_DESC": "Legt fest, ob für das Mapping der Provider Informationen das ID-Token verwendet werden soll, anstatt des Userinfo-Endpoints.", + "USEPKCE": "Verwenden Sie PKCE", + "USEPKCE_DESC": "Bestimmt, ob die Parameter code_challenge und code_challenge_method in der Authentifizierungsanforderung enthalten sind" }, "MFA": { "LIST": { @@ -2570,7 +2743,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Manager hinzufügen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 488ae68d7c..be0a3d3f17 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -185,6 +185,32 @@ "DESCRIPTION": "The idle refresh token lifetime is the maximum time a refresh token can be unused." } }, + "WEB_KEYS": { + "DESCRIPTION": "Manage your OIDC Web Keys to securely sign and validate tokens for your ZITADEL instance.", + "TABLE": { + "TITLE": "Active and Future Web Keys", + "DESCRIPTION": "Your active and upcoming web keys. Activating a new key will deactivate the current one.", + "NOTE": "Note: The JWKs OIDC endpoint returns a cacheable response (default 5 min). Avoid activating a key too soon, as it may not be available to caches and clients yet.", + "ACTIVATE": "Activate next Web Key", + "ACTIVE": "Currently active", + "NEXT": "Next in queue", + "FUTURE": "Future", + "WARNING": "Web Key is less than 5 min old" + }, + "CREATE": { + "TITLE": "Create new Web Key", + "DESCRIPTION": "Creating a new web key adds it to your list. ZITADEL uses RSA2048 keys with a SHA256 hasher by default.", + "KEY_TYPE": "Key Type", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Curve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Previous Web Keys", + "DESCRIPTION": "These are your previous web keys that are no longer active.", + "DEACTIVATED_ON": "Deactivated on" + } + }, "MESSAGE_TEXTS": { "TITLE": "Message Texts", "DESCRIPTION": "Customize the texts of your notification email or SMS messages. If you want to disable some of the languages, restrict them in your instances language settings.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Download", "APPLY": "Apply" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Actions", + "DESCRIPTION": "Actions let you run custom code in response to API requests, events or specific functions. Use them to extend Zitadel, automate workflows, and itegrate with other systems.", + "TYPES": { + "request": "Request", + "response": "Response", + "events": "Events", + "function": "Function" + }, + "DIALOG": { + "CREATE_TITLE": "Create an Action", + "UPDATE_TITLE": "Update an Action", + "TYPE": { + "DESCRIPTION": "Select when you want this Action to run", + "REQUEST": { + "TITLE": "Request", + "DESCRIPTION": "Requests that occur within Zitadel. This could be something as a login request call." + }, + "RESPONSE": { + "TITLE": "Response", + "DESCRIPTION": "A response from a request within Zitadel. Think of the response you get back from fetching a user." + }, + "EVENTS": { + "TITLE": "Events", + "DESCRIPTION": "Events that happen within Zitadel. This could be anything like a user creating an account, a successful login etc." + }, + "FUNCTIONS": { + "TITLE": "Functions", + "DESCRIPTION": "Functions that you can call within Zitadel. This could be anything from sending an email to creating a user." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Choose whether this action applies to all request, a specific service (ec. user management), or a single request (e.g. create user).", + "ALL": { + "TITLE": "All", + "DESCRIPTION": "Select this if you want to run your action on every request" + }, + "SELECT_SERVICE": { + "TITLE": "Select Service", + "DESCRIPTION": "Choose a Zitadel Service for you action." + }, + "SELECT_METHOD": { + "TITLE": "Select Method", + "DESCRIPTION": "If you want to only execute on a specific request, select it here", + "NOTE": "If you don't select a method, your action will run on every request in your selected service." + }, + "FUNCTIONNAME": { + "TITLE": "Function Name", + "DESCRIPTION": "Choose the function you want to execute" + }, + "SELECT_GROUP": { + "TITLE": "Set Group", + "DESCRIPTION": "If you want to only execute on a group of events, set the group here" + }, + "SELECT_EVENT": { + "TITLE": "Select Event", + "DESCRIPTION": "If you want to only execute on a specific event, specify it here" + } + }, + "TARGET": { + "DESCRIPTION": "You can choose to execute a target, or to run it on the same conditions as other targets.", + "TARGET": { + "DESCRIPTION": "The target you want to execute for this action" + }, + "CONDITIONS": { + "DESCRIPTION": "Execution Conditions" + } + } + }, + "TABLE": { + "CONDITION": "Condition", + "TYPE": "Type", + "TARGET": "Target", + "CREATIONDATE": "Creation Date" + } + }, + "TARGET": { + "TITLE": "Targets", + "DESCRIPTION": "A target is the destination of the code you want to execute from an action. Create a target here and at it to your actions.", + "CREATE": { + "TITLE": "Create your Target", + "DESCRIPTION": "Create your own target outside of Zitadel", + "NAME": "Name", + "NAME_DESCRIPTION": "Give your target a clear, descriptive name to make it easy to identify later", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Call", + "restAsync": "REST Async" + }, + "ENDPOINT": "Endpoint", + "ENDPOINT_DESCRIPTION": "Enter the endpoint where your code is hosted. Make sure it is accessible to us!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Set the maximum time your target has to respond. If it takes longer, we will stop the request.", + "INTERRUPT_ON_ERROR": "Interrupt on Error", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stop all executions when the targets returns with an error", + "INTERRUPT_ON_ERROR_WARNING": "Caution: “Interrupt on Error” halts operations on failure, risking lockout. Test with it disabled to prevent blocking login/creation.", + "AWAIT_RESPONSE": "Await Response", + "AWAIT_RESPONSE_DESCRIPTION": "We'll Wait for a response before we do anything else. Useful if you intend to use multiple targets for a single action" + }, + "TABLE": { + "NAME": "Name", + "ENDPOINT": "Endpoint", + "CREATIONDATE": "Creation Date", + "REORDER": "Reorder" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Has control over the whole instance, including all organizations", "IAM_OWNER_VIEWER": "Has permission to review the whole instance, including all organizations", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Has permission to create and manage users", "IAM_ADMIN_IMPERSONATOR": "Has permission to impersonate admin and end users from all organizations", "IAM_END_USER_IMPERSONATOR": "Has permission to impersonate end users from all organizations", + "IAM_LOGIN_CLIENT": "Has permission to manage login clients", "ORG_OWNER": "Has permission over the whole organization", "ORG_USER_MANAGER": "Has permission to create and manage users of the organization", "ORG_OWNER_VIEWER": "Has permission to review the whole organization", + "ORG_SETTINGS_MANAGER": "Has permission to manage organization settings", "ORG_USER_PERMISSION_EDITOR": "Has permission to manage user grants", "ORG_PROJECT_PERMISSION_EDITOR": "Has permission to manage project grants", "ORG_PROJECT_CREATOR": "Has permission to create his own projects and underlying settings", "ORG_ADMIN_IMPERSONATOR": "Has permission to impersonate admin and end users from the organization", "ORG_END_USER_IMPERSONATOR": "Has permission to impersonate end users from the organization", + "ORG_USER_SELF_MANAGER": "Has permission to manage their own user", "PROJECT_OWNER": "Has permission over the whole project", "PROJECT_OWNER_VIEWER": "Has permission to review the whole project", "PROJECT_OWNER_GLOBAL": "Has permission over the whole project", @@ -787,7 +925,10 @@ "PHONESECTION": "Phone numbers", "PASSWORDSECTION": "Initial Password", "ADDRESSANDPHONESECTION": "Phone number", - "INITMAILDESCRIPTION": "If both options are selected, no email for initialization will be sent. If only one of the options is selected, a mail to provide / verify the data will be sent." + "INITMAILDESCRIPTION": "If both options are selected, no email for initialization will be sent. If only one of the options is selected, a mail to provide / verify the data will be sent.", + "SETUPAUTHENTICATIONLATER": "Setup authentication later for this User.", + "INVITATION": "Send an invitation E-Mail for authentication setup and E-Mail verification.", + "INITIALPASSWORD": "Set an initial password for the User." }, "CODEDIALOG": { "TITLE": "Verify Phone Number", @@ -809,6 +950,9 @@ "EMAIL": "E-mail", "PHONE": "Phone number", "PHONE_HINT": "Use the + symbol followed by the calling country code, or select the country from the dropdown and finally enter the phone number", + "PHONE_VERIFIED": "Phone Number Verified", + "SEND_SMS": "Send Verification SMS", + "SEND_EMAIL": "Send E-Mail", "USERNAME": "User Name", "CHANGEUSERNAME": "modify", "CHANGEUSERNAME_TITLE": "Change username", @@ -949,6 +1093,14 @@ "5": "Suspended", "6": "Initial" }, + "STATEV2": { + "0": "Unknown", + "1": "Active", + "2": "Inactive", + "3": "Deleted", + "4": "Locked", + "5": "Initial" + }, "SEARCH": { "ADDITIONAL": "Loginname (current organization)", "ADDITIONAL-EXTERNAL": "Loginname (external organization)" @@ -1340,11 +1492,14 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "External links", "OIDC": "OIDC Token lifetime and expiration", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Secret Generator", "SECURITY": "Security settings", "EVENTS": "Events", "FAILEDEVENTS": "Failed Events", - "VIEWS": "Views" + "VIEWS": "Views", + "ACTIONS": "Actions", + "TARGETS": "Targets" }, "GROUPS": { "GENERAL": "General Information", @@ -1354,7 +1509,8 @@ "TEXTS": "Texts and Languages", "APPEARANCE": "Appearance", "OTHER": "Other", - "STORAGE": "Storage" + "STORAGE": "Storage", + "ACTIONS": "Actions" } }, "SETTING": { @@ -1385,7 +1541,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1476,6 +1633,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Session Termination", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "If the flag is enabled, the OIDC parent error will be logged in the console.", + "DISABLEUSERTOKENEVENT": "Disable User Token Event", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Enable Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "The Back-Channel Logout implements OpenID Connect Back-Channel Logout 1.0 and can be used to notify clients about session termination at the OpenID Provider.", + "PERMISSIONCHECKV2": "Permission Check V2", + "PERMISSIONCHECKV2_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", + "WEBKEY": "Web Key", + "WEBKEY_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", "STATES": { "INHERITED": "Inherit", "ENABLED": "Enabled", @@ -1486,7 +1653,12 @@ "ENABLED": "\"Enabled\" is inherited", "DISABLED": "\"Disabled\" is inherited" }, - "RESET": "Set all to inherit" + "RESET": "Set all to inherit", + "CONSOLEUSEV2USERAPI": "Use V2 Api in Console for User creation", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "When this flag is enabled, the console uses the V2 User API to create new users. With the V2 API, newly created users start without an initial state.", + "LOGINV2": "Login V2", + "LOGINV2_DESCRIPTION": "Enabling this activates the new TypeScript-based login UI with improved security, performance, and customization.", + "LOGINV2_BASEURI": "Base URI" }, "DIALOG": { "RESET": { @@ -1623,7 +1795,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Email verification done", @@ -2212,7 +2386,9 @@ "REMOVED": "Removed successfully." }, "ISIDTOKENMAPPING": "Map from the ID token", - "ISIDTOKENMAPPING_DESC": "If selected, provider information gets mapped from the ID token, not from the userinfo endpoint." + "ISIDTOKENMAPPING_DESC": "If selected, provider information gets mapped from the ID token, not from the userinfo endpoint.", + "USEPKCE": "Use PKCE", + "USEPKCE_DESC": "Determines whether the code_challenge and code_challenge_method params are included in the auth request" }, "MFA": { "LIST": { @@ -2595,7 +2771,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Add a Manager", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 09cb87a5e6..0fc95241af 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -185,6 +185,32 @@ "DESCRIPTION": "La duración de vida del token de actualización en espera es el tiempo máximo que un token de actualización puede estar sin usar." } }, + "WEB_KEYS": { + "DESCRIPTION": "Administra tus claves web OIDC para firmar y validar tokens de manera segura en tu instancia de ZITADEL.", + "TABLE": { + "TITLE": "Claves Web Activas y Futuras", + "DESCRIPTION": "Tus claves web activas y próximas. Activar una nueva clave desactivará la actual.", + "NOTE": "Nota: El endpoint JWKs OIDC devuelve una respuesta almacenable en caché (por defecto 5 min). Evita activar una clave demasiado pronto, ya que puede que aún no esté disponible en cachés y clientes.", + "ACTIVATE": "Activar la siguiente Clave Web", + "ACTIVE": "Actualmente activa", + "NEXT": "Siguiente en la cola", + "FUTURE": "Futuro", + "WARNING": "La clave web tiene menos de 5 minutos" + }, + "CREATE": { + "TITLE": "Crear nueva Clave Web", + "DESCRIPTION": "Crear una nueva clave web la añadirá a tu lista. ZITADEL usa por defecto claves RSA2048 con un algoritmo de hash SHA256.", + "KEY_TYPE": "Tipo de Clave", + "BITS": "Bits", + "HASHER": "Algoritmo de Hash", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Claves Web Anteriores", + "DESCRIPTION": "Estas son tus claves web anteriores que ya no están activas.", + "DEACTIVATED_ON": "Desactivada el" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textos de Mensajes", "DESCRIPTION": "Personaliza los textos de tus mensajes de correo electrónico de notificación o mensajes SMS. Si deseas desactivar algunos de los idiomas, restríngelos en la configuración de idiomas de tus instancias.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Descargar", "APPLY": "Aplicar" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Acciones", + "DESCRIPTION": "Las acciones te permiten ejecutar código personalizado en respuesta a solicitudes de API, eventos o funciones específicas. Úsalas para extender Zitadel, automatizar flujos de trabajo e integrarte con otros sistemas.", + "TYPES": { + "request": "Solicitud", + "response": "Respuesta", + "events": "Eventos", + "function": "Función" + }, + "DIALOG": { + "CREATE_TITLE": "Crear una acción", + "UPDATE_TITLE": "Actualizar una acción", + "TYPE": { + "DESCRIPTION": "Selecciona cuándo quieres que se ejecute esta acción", + "REQUEST": { + "TITLE": "Solicitud", + "DESCRIPTION": "Solicitudes que ocurren dentro de Zitadel. Esto podría ser algo como una llamada de solicitud de inicio de sesión." + }, + "RESPONSE": { + "TITLE": "Respuesta", + "DESCRIPTION": "Una respuesta de una solicitud dentro de Zitadel. Piensa en la respuesta que recibes al obtener un usuario." + }, + "EVENTS": { + "TITLE": "Eventos", + "DESCRIPTION": "Eventos que ocurren dentro de Zitadel. Esto podría ser cualquier cosa como un usuario creando una cuenta, un inicio de sesión exitoso, etc." + }, + "FUNCTIONS": { + "TITLE": "Funciones", + "DESCRIPTION": "Funciones que puedes llamar dentro de Zitadel. Esto podría ser cualquier cosa, desde enviar un correo electrónico hasta crear un usuario." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Elige si esta acción se aplica a todas las solicitudes, a un servicio específico (p. ej., administración de usuarios) o a una sola solicitud (p. ej., crear usuario).", + "ALL": { + "TITLE": "Todas", + "DESCRIPTION": "Selecciona esto si quieres ejecutar tu acción en cada solicitud" + }, + "SELECT_SERVICE": { + "TITLE": "Seleccionar servicio", + "DESCRIPTION": "Elige un servicio de Zitadel para tu acción." + }, + "SELECT_METHOD": { + "TITLE": "Seleccionar método", + "DESCRIPTION": "Si quieres ejecutar solo en una solicitud específica, selecciónala aquí", + "NOTE": "Si no seleccionas un método, tu acción se ejecutará en cada solicitud de tu servicio seleccionado." + }, + "FUNCTIONNAME": { + "TITLE": "Nombre de la función", + "DESCRIPTION": "Elige la función que quieres ejecutar" + }, + "SELECT_GROUP": { + "TITLE": "Establecer grupo", + "DESCRIPTION": "Si quieres ejecutar solo en un grupo de eventos, establece el grupo aquí" + }, + "SELECT_EVENT": { + "TITLE": "Seleccionar evento", + "DESCRIPTION": "Si quieres ejecutar solo en un evento específico, especifícalo aquí" + } + }, + "TARGET": { + "DESCRIPTION": "Puedes elegir ejecutar un objetivo o ejecutarlo en las mismas condiciones que otros objetivos.", + "TARGET": { + "DESCRIPTION": "El objetivo que quieres ejecutar para esta acción" + }, + "CONDITIONS": { + "DESCRIPTION": "Condiciones de ejecución" + } + } + }, + "TABLE": { + "CONDITION": "Condición", + "TYPE": "Tipo", + "TARGET": "Objetivo", + "CREATIONDATE": "Fecha de creación" + } + }, + "TARGET": { + "TITLE": "Objetivos", + "DESCRIPTION": "Un objetivo es el destino del código que quieres ejecutar desde una acción. Crea un objetivo aquí y agrégalo a tus acciones.", + "CREATE": { + "TITLE": "Crear tu objetivo", + "DESCRIPTION": "Crea tu propio objetivo fuera de Zitadel", + "NAME": "Nombre", + "NAME_DESCRIPTION": "Dale a tu objetivo un nombre claro y descriptivo para que sea fácil de identificar más tarde", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Llamada REST", + "restAsync": "REST Asíncrono" + }, + "ENDPOINT": "Punto de conexión", + "ENDPOINT_DESCRIPTION": "Introduce el punto de conexión donde se aloja tu código. ¡Asegúrate de que sea accesible para nosotros!", + "TIMEOUT": "Tiempo de espera", + "TIMEOUT_DESCRIPTION": "Establece el tiempo máximo que tiene tu objetivo para responder. Si tarda más, detendremos la solicitud.", + "INTERRUPT_ON_ERROR": "Interrumpir en error", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Detén todas las ejecuciones cuando los objetivos devuelvan un error", + "INTERRUPT_ON_ERROR_WARNING": "Precaución: “Interrumpir en error” detiene las operaciones si fallan, lo que puede provocar un bloqueo. Pruebe con esta opción desactivada para evitar bloquear el inicio de sesión o la creación.", + "AWAIT_RESPONSE": "Esperar respuesta", + "AWAIT_RESPONSE_DESCRIPTION": "Esperaremos una respuesta antes de hacer cualquier otra cosa. Útil si tienes la intención de usar múltiples objetivos para una sola acción" + }, + "TABLE": { + "NAME": "Nombre", + "ENDPOINT": "Punto de conexión", + "CREATIONDATE": "Fecha de creación", + "REORDER": "Reordenar" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Tiene control sobre toda la instancia, incluyendo todas las organizaciones", "IAM_OWNER_VIEWER": "Tiene permiso para revisar toda la instancia, incluyendo todas las organizaciones", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Tiene permiso para crear y gestionar usuarios", "IAM_ADMIN_IMPERSONATOR": "Tiene permiso para hacerse pasar por administradores y usuarios finales de todas las organizaciones", "IAM_END_USER_IMPERSONATOR": "Tiene permiso para hacerse pasar por usuarios finales de todas las organizaciones", + "IAM_LOGIN_CLIENT": "Tiene permiso para gestionar los clientes de inicio de sesión", "ORG_OWNER": "Tiene permisos sobre toda la organización", "ORG_USER_MANAGER": "Tiene permiso para crear y gestionar usuarios de la organización", "ORG_OWNER_VIEWER": "TIene permiso para revisar toda la organización", + "ORG_SETTINGS_MANAGER": "Tiene permiso para gestionar las configuraciones de la organización", "ORG_USER_PERMISSION_EDITOR": "Tiene permiso para gestionar concesiones de usuario", "ORG_PROJECT_PERMISSION_EDITOR": "Tiene permiso para gestionar concesiones de proyecto", "ORG_PROJECT_CREATOR": "Tiene permiso para crear sus propios proyectos y ajustes subyacentes", "ORG_ADMIN_IMPERSONATOR": "Tiene permiso para hacerse pasar por administradores y usuarios finales de la organización", "ORG_END_USER_IMPERSONATOR": "Tiene permiso para hacerse pasar por usuarios finales de la organización", + "ORG_USER_SELF_MANAGER": "Tiene permiso para gestionar su propio usuario", "PROJECT_OWNER": "Tiene permiso sobre todo el proyecto", "PROJECT_OWNER_VIEWER": "Tiene permiso para revisar todo el proyecto", "PROJECT_OWNER_GLOBAL": "Tiene permiso sobre todo el proyecto", @@ -787,7 +925,10 @@ "PHONESECTION": "Números de teléfono", "PASSWORDSECTION": "Contraseña inicial", "ADDRESSANDPHONESECTION": "Número de teléfono", - "INITMAILDESCRIPTION": "Si ambas opciones se seleccionan, no se enviará un email para la inicialización. Si solo una de las opciones se selecciona, un email se enviará para proporcionar / verificar los datos." + "INITMAILDESCRIPTION": "Si ambas opciones se seleccionan, no se enviará un email para la inicialización. Si solo una de las opciones se selecciona, un email se enviará para proporcionar / verificar los datos.", + "SETUPAUTHENTICATIONLATER": "Configurar la autenticación más tarde para este usuario.", + "INVITATION": "Enviar un correo de invitación para la configuración de autenticación y verificación de correo electrónico.", + "INITIALPASSWORD": "Establece una contraseña inicial para el usuario." }, "CODEDIALOG": { "TITLE": "Verificar número de teléfono", @@ -809,6 +950,9 @@ "EMAIL": "Email", "PHONE": "Número de teléfono", "PHONE_HINT": "Usa el símbolo + seguido del prefijo del país o selecciona el país del menú desplegable y finalmente introduce el número de teléfono", + "PHONE_VERIFIED": "Número de teléfono verificado", + "SEND_SMS": "Enviar SMS de verificación", + "SEND_EMAIL": "Enviar correo electrónico", "USERNAME": "Nombre de usuario", "CHANGEUSERNAME": "modificar", "CHANGEUSERNAME_TITLE": "Cambiar nombre de usuario", @@ -949,6 +1093,14 @@ "5": "Suspendido", "6": "Inicial" }, + "STATEV2": { + "0": "Desconocido", + "1": "Activo", + "2": "Inactivo", + "3": "Borrado", + "4": "Bloqueado", + "5": "Inicial" + }, "SEARCH": { "ADDITIONAL": "Nombre de inicio de sesión (organización actual)", "ADDITIONAL-EXTERNAL": "Nombre de inicio de sesión (organización externa)" @@ -1341,6 +1493,7 @@ "BRANDING": "Imagen de marca", "PRIVACYPOLICY": "Política de privacidad", "OIDC": "OIDC Token lifetime and expiration", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Apariencia del secreto", "SECURITY": "Ajustes de seguridad", "EVENTS": "Eventos", @@ -1386,7 +1539,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1477,6 +1631,16 @@ "ACTIONS_DESCRIPTION": "Acciones v2 permite administrar las ejecuciones y objetivos de datos. Si la bandera está habilitada, podrá utilizar la nueva API y sus funciones.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Terminación de sesión", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Si la bandera está habilitada, podrá terminar una sesión única desde la interfaz de usuario de inicio de sesión proporcionando un id_token con una reclamación `sid` como id_token_hint en el punto final de end_session. Tenga en cuenta que actualmente se terminan todas las sesiones del mismo agente de usuario (navegador) en la interfaz de usuario de inicio de sesión. Las sesiones administradas a través de la API de sesión ya permiten la terminación de sesiones individuales.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Si la bandera está habilitada, el error del padre OIDC se registrará en la consola.", + "DISABLEUSERTOKENEVENT": "Desactivar evento de token de usuario", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Habilitar Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "El Back-Channel Logout implementa OpenID Connect Back-Channel Logout 1.0 y se puede usar para notificar a los clientes sobre la terminación de la sesión en el proveedor de OpenID.", + "PERMISSIONCHECKV2": "Verificación de permisos V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Si la bandera está habilitada, podrá usar la nueva API y sus funciones.", + "WEBKEY": "Clave web", + "WEBKEY_DESCRIPTION": "Si la bandera está habilitada, podrá usar la nueva API y sus funciones.", "STATES": { "INHERITED": "Heredado", "ENABLED": "Habilitado", @@ -1487,7 +1651,12 @@ "ENABLED": "\"Habilitado\" se hereda", "DISABLED": "\"Deshabilitado\" se hereda" }, - "RESET": "Establecer todo a heredado" + "RESET": "Establecer todo a heredado", + "CONSOLEUSEV2USERAPI": "Utilice la API V2 en la consola para la creación de usuarios", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Cuando esta opción está habilitada, la consola utiliza la API V2 de usuario para crear nuevos usuarios. Con la API V2, los usuarios recién creados comienzan sin un estado inicial.", + "LOGINV2": "Inicio de sesión V2", + "LOGINV2_DESCRIPTION": "Al habilitar esto, se activa la nueva interfaz de inicio de sesión basada en TypeScript con mejoras en seguridad, rendimiento y personalización.", + "LOGINV2_BASEURI": "URI base" }, "DIALOG": { "RESET": { @@ -1624,7 +1793,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Verificación de email realizada", @@ -2188,7 +2359,9 @@ "REMOVED": "Eliminado con éxito." }, "ISIDTOKENMAPPING": "Asignación del ID token", - "ISIDTOKENMAPPING_DESC": "Si se selecciona, la información del proveedor se asigna desde el ID token, no desde el punto final de userinfo." + "ISIDTOKENMAPPING_DESC": "Si se selecciona, la información del proveedor se asigna desde el ID token, no desde el punto final de userinfo.", + "USEPKCE": "Usa PKCE", + "USEPKCE_DESC": "Determina si los parámetros code_challenge y code_challenge_method son incluidos en la solicitud de autenticación." }, "MFA": { "LIST": { @@ -2567,7 +2740,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Añadir un Mánager", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index c5ae8d767c..60a0a3e482 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -185,6 +185,32 @@ "DESCRIPTION": "La durée de vie du token de rafraîchissement inactif est le temps maximum qu'un token de rafraîchissement peut être inutilisé." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gérez vos clés Web OIDC pour signer et valider en toute sécurité les jetons de votre instance ZITADEL.", + "TABLE": { + "TITLE": "Clés Web Actives et Futures", + "DESCRIPTION": "Vos clés Web actives et à venir. L'activation d'une nouvelle clé désactivera l'actuelle.", + "NOTE": "Remarque : Le point de terminaison JWKs OIDC renvoie une réponse mise en cache (par défaut 5 min). Évitez d'activer une clé trop tôt, car elle pourrait ne pas encore être disponible pour les caches et les clients.", + "ACTIVATE": "Activer la prochaine Clé Web", + "ACTIVE": "Actuellement active", + "NEXT": "Prochaine dans la file d'attente", + "FUTURE": "Futur", + "WARNING": "La clé Web a moins de 5 minutes" + }, + "CREATE": { + "TITLE": "Créer une nouvelle Clé Web", + "DESCRIPTION": "Créer une nouvelle clé Web l'ajoutera à votre liste. ZITADEL utilise par défaut des clés RSA2048 avec un hacheur SHA256.", + "KEY_TYPE": "Type de Clé", + "BITS": "Bits", + "HASHER": "Hacheur", + "CURVE": "Courbe" + }, + "PREVIOUS_TABLE": { + "TITLE": "Clés Web Précédentes", + "DESCRIPTION": "Voici vos anciennes clés Web qui ne sont plus actives.", + "DEACTIVATED_ON": "Désactivée le" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textes des Messages", "DESCRIPTION": "Personnalisez les textes de vos e-mails de notification ou messages SMS. Si vous souhaitez désactiver certaines langues, restreignez-les dans les paramètres de langue de vos instances.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Télécharger", "APPLY": "Appliquer" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Actions", + "DESCRIPTION": "Les actions vous permettent d'exécuter du code personnalisé en réponse à des requêtes API, des événements ou des fonctions spécifiques. Utilisez-les pour étendre Zitadel, automatiser les flux de travail et vous intégrer à d'autres systèmes.", + "TYPES": { + "request": "Requête", + "response": "Réponse", + "events": "Événements", + "function": "Fonction" + }, + "DIALOG": { + "CREATE_TITLE": "Créer une action", + "UPDATE_TITLE": "Mettre à jour une action", + "TYPE": { + "DESCRIPTION": "Sélectionnez quand vous souhaitez que cette action s'exécute", + "REQUEST": { + "TITLE": "Requête", + "DESCRIPTION": "Requêtes qui se produisent dans Zitadel. Cela pourrait être quelque chose comme un appel de requête de connexion." + }, + "RESPONSE": { + "TITLE": "Réponse", + "DESCRIPTION": "Une réponse à une requête dans Zitadel. Pensez à la réponse que vous obtenez lorsque vous récupérez un utilisateur." + }, + "EVENTS": { + "TITLE": "Événements", + "DESCRIPTION": "Événements qui se produisent dans Zitadel. Cela pourrait être n'importe quoi, comme un utilisateur créant un compte, une connexion réussie, etc." + }, + "FUNCTIONS": { + "TITLE": "Fonctions", + "DESCRIPTION": "Fonctions que vous pouvez appeler dans Zitadel. Cela pourrait être n'importe quoi, de l'envoi d'un e-mail à la création d'un utilisateur." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Choisissez si cette action s'applique à toutes les requêtes, à un service spécifique (par exemple, la gestion des utilisateurs) ou à une seule requête (par exemple, créer un utilisateur).", + "ALL": { + "TITLE": "Tous", + "DESCRIPTION": "Sélectionnez ceci si vous souhaitez exécuter votre action sur chaque requête" + }, + "SELECT_SERVICE": { + "TITLE": "Sélectionner un service", + "DESCRIPTION": "Choisissez un service Zitadel pour votre action." + }, + "SELECT_METHOD": { + "TITLE": "Sélectionner une méthode", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur une requête spécifique, sélectionnez-la ici", + "NOTE": "Si vous ne sélectionnez pas de méthode, votre action s'exécutera sur chaque requête de votre service sélectionné." + }, + "FUNCTIONNAME": { + "TITLE": "Nom de la fonction", + "DESCRIPTION": "Choisissez la fonction que vous souhaitez exécuter" + }, + "SELECT_GROUP": { + "TITLE": "Définir un groupe", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur un groupe d'événements, définissez le groupe ici" + }, + "SELECT_EVENT": { + "TITLE": "Sélectionner un événement", + "DESCRIPTION": "Si vous souhaitez exécuter uniquement sur un événement spécifique, spécifiez-le ici" + } + }, + "TARGET": { + "DESCRIPTION": "Vous pouvez choisir d'exécuter une cible ou de l'exécuter dans les mêmes conditions que d'autres cibles.", + "TARGET": { + "DESCRIPTION": "La cible que vous souhaitez exécuter pour cette action" + }, + "CONDITIONS": { + "DESCRIPTION": "Conditions d'exécution" + } + } + }, + "TABLE": { + "CONDITION": "Condition", + "TYPE": "Type", + "TARGET": "Cible", + "CREATIONDATE": "Date de création" + } + }, + "TARGET": { + "TITLE": "Cibles", + "DESCRIPTION": "Une cible est la destination du code que vous souhaitez exécuter à partir d'une action. Créez une cible ici et ajoutez-la à vos actions.", + "CREATE": { + "TITLE": "Créer votre cible", + "DESCRIPTION": "Créez votre propre cible en dehors de Zitadel", + "NAME": "Nom", + "NAME_DESCRIPTION": "Donnez à votre cible un nom clair et descriptif pour la rendre facile à identifier plus tard", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Appel REST", + "restAsync": "REST Asynchrone" + }, + "ENDPOINT": "Point de terminaison", + "ENDPOINT_DESCRIPTION": "Entrez le point de terminaison où votre code est hébergé. Assurez-vous qu'il nous est accessible !", + "TIMEOUT": "Délai d'attente", + "TIMEOUT_DESCRIPTION": "Définissez le temps maximal dont votre cible dispose pour répondre. Si cela prend plus de temps, nous arrêterons la requête.", + "INTERRUPT_ON_ERROR": "Interrompre en cas d'erreur", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Arrêtez toutes les exécutions lorsque les cibles renvoient une erreur", + "INTERRUPT_ON_ERROR_WARNING": "Attention : “Interrompre en cas d'erreur” arrête les opérations en cas d’échec, ce qui peut entraîner un verrouillage. Testez avec cette option désactivée pour éviter de bloquer la connexion ou la création.", + "AWAIT_RESPONSE": "Attendre une réponse", + "AWAIT_RESPONSE_DESCRIPTION": "Nous attendrons une réponse avant de faire autre chose. Utile si vous avez l'intention d'utiliser plusieurs cibles pour une seule action" + }, + "TABLE": { + "NAME": "Nom", + "ENDPOINT": "Point de terminaison", + "CREATIONDATE": "Date de création", + "REORDER": "Réorganiser" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "A le contrôle de toute l'instance, y compris toutes les organisations", "IAM_OWNER_VIEWER": "A le droit de passer en revue l'ensemble de l'instance, y compris toutes les organisations.", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "A le droit de créer et de gérer les utilisateurs", "IAM_ADMIN_IMPERSONATOR": "A l'autorisation de se faire passer pour l'administrateur et les utilisateurs finaux de toutes les organisations", "IAM_END_USER_IMPERSONATOR": "Est autorisé à usurper l'identité des utilisateurs finaux de toutes les organisations", + "IAM_LOGIN_CLIENT": "A la permission de gérer les clients de connexion", "ORG_OWNER": "A le droit de contrôler l'ensemble de l'organisation", "ORG_USER_MANAGER": "A le droit de créer et de gérer les utilisateurs de l'organisation", "ORG_OWNER_VIEWER": "A le droit de passer en revue l'ensemble de l'organisation", + "ORG_SETTINGS_MANAGER": "A le droit de gérer les paramètres de l'organisation", "ORG_USER_PERMISSION_EDITOR": "A le droit d'octroyer des droits aux utilisateurs", "ORG_PROJECT_PERMISSION_EDITOR": "A le droit d'octroyer des droits aux projets", "ORG_PROJECT_CREATOR": "A le droit de créer ses propres projets et leurs paramètres sous-jacents.", "ORG_ADMIN_IMPERSONATOR": "A l'autorisation de se faire passer pour l'administrateur et les utilisateurs finaux de l'organisation", "ORG_END_USER_IMPERSONATOR": "Est autorisé à usurper l'identité des utilisateurs finaux de l'organisation", + "ORG_USER_SELF_MANAGER": "A le droit de gérer ses propres utilisateurs", "PROJECT_OWNER": "A le droit de gérer l'ensemble du projet", "PROJECT_OWNER_VIEWER": "A le droit de passer en revue l'ensemble du projet", "PROJECT_OWNER_GLOBAL": "A le droit d'accéder à l'ensemble du projet", @@ -787,7 +925,10 @@ "PHONESECTION": "Numéro de téléphone", "PASSWORDSECTION": "Mot de passe initial", "ADDRESSANDPHONESECTION": "Numéro de téléphone", - "INITMAILDESCRIPTION": "Si les deux options sont sélectionnées, aucun mail d'initialisation ne sera envoyé. Si une seule des options est sélectionnée, un mail pour fournir / vérifier les données sera envoyé." + "INITMAILDESCRIPTION": "Si les deux options sont sélectionnées, aucun mail d'initialisation ne sera envoyé. Si une seule des options est sélectionnée, un mail pour fournir / vérifier les données sera envoyé.", + "SETUPAUTHENTICATIONLATER": "Configurer l'authentification plus tard pour cet utilisateur.", + "INVITATION": "Envoyer un e-mail d'invitation pour la configuration de l'authentification et la vérification de l'e-mail.", + "INITIALPASSWORD": "Définissez un mot de passe initial pour l'utilisateur." }, "CODEDIALOG": { "TITLE": "Vérifier le numéro de téléphone", @@ -809,6 +950,9 @@ "EMAIL": "Courriel", "PHONE": "Numéro de téléphone", "PHONE_HINT": "Utilisez le symbole + suivi de l'indicatif du pays de l'appelant, ou sélectionnez le pays dans la liste déroulante et saisissez enfin le numéro de téléphone.", + "PHONE_VERIFIED": "Numéro de téléphone vérifié", + "SEND_SMS": "Envoyer un SMS de vérification", + "SEND_EMAIL": "Envoyer un Courriel", "USERNAME": "Nom de l'utilisateur", "CHANGEUSERNAME": "modifier", "CHANGEUSERNAME_TITLE": "Modifier le nom d'utilisateur", @@ -949,6 +1093,14 @@ "5": "Suspendu", "6": "Initial" }, + "STATEV2": { + "0": "Inconnu", + "1": "Actif", + "2": "Inactif", + "3": "Supprimé", + "4": "Verrouillé", + "5": "Initial" + }, "SEARCH": { "ADDITIONAL": "Nom de connexion (organisation actuelle)", "ADDITIONAL-EXTERNAL": "Nom de connexion (organisation externe)" @@ -1340,6 +1492,7 @@ "BRANDING": "Image de marque", "PRIVACYPOLICY": "Politique de confidentialité", "OIDC": "Durée de vie et expiration des jetons OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Générateur de secrets", "SECURITY": "Paramètres de sécurité", "EVENTS": "Événements", @@ -1385,7 +1538,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1476,6 +1630,16 @@ "ACTIONS_DESCRIPTION": "Les actions v2 permettent de gérer les exécutions et les cibles de données. Si l'indicateur est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Fin de session", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Si l'indicateur est activé, vous pourrez terminer une seule session à partir de l'interface utilisateur de connexion en fournissant un id_token avec une revendication `sid` en tant que id_token_hint sur le point de terminaison end_session. Notez que toutes les sessions du même agent utilisateur (navigateur) sont actuellement terminées dans l'interface utilisateur de connexion. Les sessions gérées via l'API de session permettent déjà la terminaison de sessions individuelles.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Si le drapeau est activé, l'erreur parent OIDC sera enregistrée dans la console.", + "DISABLEUSERTOKENEVENT": "Désactiver l'événement de jeton utilisateur", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Activer le Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Le Back-Channel Logout implémente OpenID Connect Back-Channel Logout 1.0 et peut être utilisé pour notifier les clients de la fin de session chez le fournisseur OpenID.", + "PERMISSIONCHECKV2": "Vérification des permissions V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", + "WEBKEY": "Clé web", + "WEBKEY_DESCRIPTION": "Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", "STATES": { "INHERITED": "Hérité", "ENABLED": "Activé", @@ -1486,7 +1650,12 @@ "ENABLED": "\"Activé\" est hérité", "DISABLED": "\"Désactivé\" est hérité" }, - "RESET": "Réinitialiser tout sur hérité" + "RESET": "Réinitialiser tout sur hérité", + "CONSOLEUSEV2USERAPI": "Utilisez l'API V2 dans la console pour la création d'utilisateurs", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Lorsque ce drapeau est activé, la console utilise l'API V2 User pour créer de nouveaux utilisateurs. Avec l'API V2, les nouveaux utilisateurs commencent sans état initial.", + "LOGINV2": "Connexion V2", + "LOGINV2_DESCRIPTION": "L’activation de cette option lance la nouvelle interface de connexion basée sur TypeScript, avec une sécurité, des performances et une personnalisation améliorées.", + "LOGINV2_BASEURI": "URI de base" }, "DIALOG": { "RESET": { @@ -1623,7 +1792,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Vérification de l'e-mail effectuée", @@ -2192,7 +2363,9 @@ "REMOVED": "Suppression réussie." }, "ISIDTOKENMAPPING": "Mappage depuis le jeton ID", - "ISIDTOKENMAPPING_DESC": "Si sélectionné, les informations du fournisseur sont mappées à partir du jeton ID, et non à partir du point d'extrémité userinfo." + "ISIDTOKENMAPPING_DESC": "Si sélectionné, les informations du fournisseur sont mappées à partir du jeton ID, et non à partir du point d'extrémité userinfo.", + "USEPKCE": "Utiliser PKCE", + "USEPKCE_DESC": "Détermine si les paramètres code_challenge et code_challenge_method sont inclus dans la demande d'authentification" }, "MFA": { "LIST": { @@ -2571,7 +2744,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Ajouter un responsable", diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json index e74e9eab32..5724c45a51 100644 --- a/console/src/assets/i18n/hu.json +++ b/console/src/assets/i18n/hu.json @@ -185,6 +185,32 @@ "DESCRIPTION": "A tétlen frissítő token élettartama az a maximális idő, ameddig a frissítő token használaton kívül maradhat." } }, + "WEB_KEYS": { + "DESCRIPTION": "Kezelje az OIDC Webkulcsokat, hogy biztonságosan aláírja és érvényesítse a tokeneket a ZITADEL példányában.", + "TABLE": { + "TITLE": "Aktív és Jövőbeli Webkulcsok", + "DESCRIPTION": "Az aktív és közelgő webkulcsai. Egy új kulcs aktiválása deaktiválja az aktuálisat.", + "NOTE": "Megjegyzés: A JWKs OIDC végpont egy gyorsítótárazható választ ad vissza (alapértelmezett: 5 perc). Kerülje a kulcs túl korai aktiválását, mivel lehet, hogy még nem érhető el a gyorsítótárakban és a klienseknél.", + "ACTIVATE": "Következő Webkulcs aktiválása", + "ACTIVE": "Jelenleg aktív", + "NEXT": "Következő a sorban", + "FUTURE": "Jövőbeli", + "WARNING": "A webkulcs kevesebb mint 5 perces" + }, + "CREATE": { + "TITLE": "Új Webkulcs létrehozása", + "DESCRIPTION": "Egy új webkulcs létrehozása hozzáadja azt a listájához. A ZITADEL alapértelmezés szerint RSA2048 kulcsokat használ SHA256 hasheléssel.", + "KEY_TYPE": "Kulcstípus", + "BITS": "Bitek", + "HASHER": "Hasher", + "CURVE": "Görbe" + }, + "PREVIOUS_TABLE": { + "TITLE": "Korábbi Webkulcsok", + "DESCRIPTION": "Ezek a korábbi webkulcsai, amelyek már nem aktívak.", + "DEACTIVATED_ON": "Deaktiválva ekkor" + } + }, "MESSAGE_TEXTS": { "TITLE": "Üzenet Szövegek", "DESCRIPTION": "Testreszabhatod az értesítési e-mailjeid vagy SMS üzeneteid szövegeit. Ha le szeretnél tiltani néhány nyelvet, korlátozd azokat az instance nyelvi beállításaiban.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Letöltés", "APPLY": "Alkalmaz" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Műveletek", + "DESCRIPTION": "A műveletek lehetővé teszik egyedi kód futtatását API-kérésekre, eseményekre vagy konkrét függvényekre válaszul. Használja őket a Zitadel kiterjesztéséhez, a munkafolyamatok automatizálásához és más rendszerekkel való integrációhoz.", + "TYPES": { + "request": "Kérés", + "response": "Válasz", + "events": "Események", + "function": "Függvény" + }, + "DIALOG": { + "CREATE_TITLE": "Művelet létrehozása", + "UPDATE_TITLE": "Művelet frissítése", + "TYPE": { + "DESCRIPTION": "Válassza ki, mikor szeretné futtatni ezt a műveletet", + "REQUEST": { + "TITLE": "Kérés", + "DESCRIPTION": "A Zitadelen belül előforduló kérések. Ez lehet például egy bejelentkezési kérés hívása." + }, + "RESPONSE": { + "TITLE": "Válasz", + "DESCRIPTION": "Válasz egy Zitadelen belüli kérésre. Gondoljon arra a válaszra, amelyet egy felhasználó lekérésekor kap." + }, + "EVENTS": { + "TITLE": "Események", + "DESCRIPTION": "A Zitadelen belül zajló események. Ez bármi lehet, például egy felhasználó fiók létrehozása, sikeres bejelentkezés stb." + }, + "FUNCTIONS": { + "TITLE": "Függvények", + "DESCRIPTION": "Függvények, amelyeket a Zitadelen belül hívhat. Ez bármi lehet, az e-mail küldésétől a felhasználó létrehozásáig." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Válassza ki, hogy ez a művelet vonatkozik-e minden kérésre, egy adott szolgáltatásra (pl. felhasználókezelés) vagy egyetlen kérésre (pl. felhasználó létrehozása).", + "ALL": { + "TITLE": "Összes", + "DESCRIPTION": "Válassza ezt, ha minden kérésnél futtatni szeretné a műveletet" + }, + "SELECT_SERVICE": { + "TITLE": "Szolgáltatás kiválasztása", + "DESCRIPTION": "Válasszon egy Zitadel szolgáltatást a művelethez." + }, + "SELECT_METHOD": { + "TITLE": "Módszer kiválasztása", + "DESCRIPTION": "Ha csak egy adott kérésnél szeretne futtatni, válassza ki itt", + "NOTE": "Ha nem választ módszert, a művelet minden kérésnél futni fog a kiválasztott szolgáltatásban." + }, + "FUNCTIONNAME": { + "TITLE": "Függvénynév", + "DESCRIPTION": "Válassza ki a futtatni kívánt függvényt" + }, + "SELECT_GROUP": { + "TITLE": "Csoport beállítása", + "DESCRIPTION": "Ha csak események egy csoportján szeretne futtatni, állítsa be itt a csoportot" + }, + "SELECT_EVENT": { + "TITLE": "Esemény kiválasztása", + "DESCRIPTION": "Ha csak egy adott eseményen szeretne futtatni, adja meg itt" + } + }, + "TARGET": { + "DESCRIPTION": "Választhat, hogy futtat egy célt, vagy ugyanazokkal a feltételekkel futtatja, mint más célokat.", + "TARGET": { + "DESCRIPTION": "A cél, amelyet futtatni szeretne ehhez a művelethez" + }, + "CONDITIONS": { + "DESCRIPTION": "Végrehajtási feltételek" + } + } + }, + "TABLE": { + "CONDITION": "Feltétel", + "TYPE": "Típus", + "TARGET": "Cél", + "CREATIONDATE": "Létrehozás dátuma" + } + }, + "TARGET": { + "TITLE": "Célok", + "DESCRIPTION": "A cél annak a kódnak a célja, amelyet egy műveletből szeretne futtatni. Hozzon létre itt egy célt, és adja hozzá a műveleteihez.", + "CREATE": { + "TITLE": "Cél létrehozása", + "DESCRIPTION": "Hozza létre saját célját a Zitadelen kívül", + "NAME": "Név", + "NAME_DESCRIPTION": "Adjon a céljának egy világos, leíró nevet, hogy később könnyen azonosítható legyen", + "TYPE": "Típus", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Hívás", + "restAsync": "REST Aszinkron" + }, + "ENDPOINT": "Végpont", + "ENDPOINT_DESCRIPTION": "Adja meg azt a végpontot, ahol a kódja található. Győződjön meg arról, hogy elérhető számunkra!", + "TIMEOUT": "Időtúllépés", + "TIMEOUT_DESCRIPTION": "Állítsa be a maximális időt, amíg a céljának válaszolnia kell. Ha tovább tart, leállítjuk a kérést.", + "INTERRUPT_ON_ERROR": "Hiba esetén megszakítás", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Állítsa le az összes végrehajtást, ha a célok hibát adnak vissza", + "INTERRUPT_ON_ERROR_WARNING": "Figyelem: Az „Hiba esetén megszakítás” funkció leállítja a műveleteket hiba esetén, ami kizáráshoz vezethet. Tesztelje kikapcsolt állapotban a bejelentkezés/létrehozás blokkolásának elkerülése érdekében.", + "AWAIT_RESPONSE": "Válaszra várás", + "AWAIT_RESPONSE_DESCRIPTION": "Megvárjuk a választ, mielőtt bármi mást tennénk. Hasznos, ha több célt szeretne használni egyetlen művelethez" + }, + "TABLE": { + "NAME": "Név", + "ENDPOINT": "Végpont", + "CREATIONDATE": "Létrehozás dátuma", + "REORDER": "Újrarendelés" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Teljes irányítása van az egész példány felett, beleértve minden szervezetet", "IAM_OWNER_VIEWER": "Jogosultsága van az egész példány átnézésére, beleértve minden szervezetet", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Jogosultsága van felhasználók létrehozására és kezelésére", "IAM_ADMIN_IMPERSONATOR": "Jogosultsága van adminok és végfelhasználók megszemélyesítésére minden szervezetből", "IAM_END_USER_IMPERSONATOR": "Engedélye van az összes szervezet véghasználóinak megszemélyesítésére", + "IAM_LOGIN_CLIENT": "Jogosultsága van a bejelentkezési kliensek kezelésére", "ORG_OWNER": "Engedélye van az egész szervezet fölött", "ORG_USER_MANAGER": "Engedélye van a szervezet felhasználóinak létrehozására és kezelésére", "ORG_OWNER_VIEWER": "Engedélye van az egész szervezet áttekintésére", + "ORG_SETTINGS_MANAGER": "Engedélye van a szervezet beállításainak kezelésére", "ORG_USER_PERMISSION_EDITOR": "Engedélye van a felhasználói jogok kezelésére", "ORG_PROJECT_PERMISSION_EDITOR": "Engedélye van a projektjogosultságok kezelésére", "ORG_PROJECT_CREATOR": "Engedélye van saját projektek és alapbeállítások létrehozására", "ORG_ADMIN_IMPERSONATOR": "Engedélye van a szervezet adminisztrátorainak és véghasználóinak megszemélyesítésére", "ORG_END_USER_IMPERSONATOR": "Engedélye van a szervezet véghasználóinak megszemélyesítésére", + "ORG_USER_SELF_MANAGER": "Engedélye van a saját felhasználói fiókjaidat kezelésére", "PROJECT_OWNER": "Engedélye van az egész projekt fölött", "PROJECT_OWNER_VIEWER": "Jogosultságod van a teljes projekt átnézésére.", "PROJECT_OWNER_GLOBAL": "Jogosultságod van a teljes projektre.", @@ -787,7 +925,10 @@ "PHONESECTION": "Telefonszámok", "PASSWORDSECTION": "Kezdeti jelszó", "ADDRESSANDPHONESECTION": "Telefonszám", - "INITMAILDESCRIPTION": "Ha mindkét opció ki van választva, nem kerül kiküldésre inicializáló e-mail. Ha csak az egyik opció van kiválasztva, egy e-mailt küldünk az adatok megadására / ellenőrzésére." + "INITMAILDESCRIPTION": "Ha mindkét opció ki van választva, nem kerül kiküldésre inicializáló e-mail. Ha csak az egyik opció van kiválasztva, egy e-mailt küldünk az adatok megadására / ellenőrzésére.", + "SETUPAUTHENTICATIONLATER": "Állítsa be később az autentikációt ehhez a felhasználóhoz.", + "INVITATION": "Küldjön meghívó e-mailt az autentikáció beállításához és az e-mail hitelesítéséhez.", + "INITIALPASSWORD": "Állítson be egy kezdeti jelszót a felhasználónak." }, "CODEDIALOG": { "TITLE": "Telefonszám ellenőrzése", @@ -809,6 +950,9 @@ "EMAIL": "E-mail", "PHONE": "Telefonszám", "PHONE_HINT": "Használd a + szimbólumot az ország hívókódja előtt, vagy válaszd ki az országot a legördülő menüből, és végül add meg a telefonszámot", + "PHONE_VERIFIED": "Telefonszám ellenőrizve", + "SEND_SMS": "Ellenőrző SMS küldése", + "SEND_EMAIL": "E-mail küldése", "USERNAME": "Felhasználónév", "CHANGEUSERNAME": "módosít", "CHANGEUSERNAME_TITLE": "Felhasználónév megváltoztatása", @@ -949,6 +1093,14 @@ "5": "Felfüggesztett", "6": "Kezdeti" }, + "STATEV2": { + "0": "Ismeretlen", + "1": "Aktív", + "2": "Inaktív", + "3": "Törölt", + "4": "Zárolt", + "5": "Kezdeti" + }, "SEARCH": { "ADDITIONAL": "Belépési név (jelenlegi szervezet)", "ADDITIONAL-EXTERNAL": "Belépési név (külső szervezet)" @@ -1340,6 +1492,7 @@ "BRANDING": "Márkaépítés", "PRIVACYPOLICY": "Külső hivatkozások", "OIDC": "OIDC token élettartam és lejárat", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Titokgenerátor", "SECURITY": "Biztonsági beállítások", "EVENTS": "Események", @@ -1385,7 +1538,8 @@ "sv": "Svéd", "id": "Indonéz", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1474,6 +1628,16 @@ "ACTIONS_DESCRIPTION": "A Műveletek v2 lehetővé teszik az adat futtatások és célok kezelését. Ha az opció engedélyezve van, használhatod az új API-t és annak funkcióit.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Egyedüli V1 Munkamenet Befejezése", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Ha a zászló engedélyezve van, képes leszel egyetlen munkamenetet megszüntetni a bejelentkezési UI-ben, ha egy id_tokent biztosítasz egy `sid` claim-mel mint id_token_hint az end_session végpontnál. Megjegyzendő, hogy jelenleg az összes munkamenet ugyanabból a felhasználói ügynökből (böngésző) leállításra kerül a bejelentkezési UI-ben. A Session API-val kezelt munkamenetek már lehetővé teszik egyes munkamenetek megszüntetését.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Ha a zászló engedélyezve van, az OIDC szülő hiba naplózva lesz a konzolban.", + "DISABLEUSERTOKENEVENT": "Felhasználói token esemény letiltása", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Backchannel Logout engedélyezése", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "A Back-Channel Logout megvalósítja az OpenID Connect Back-Channel Logout 1.0-t, és használható az ügyfelek értesítésére a munkamenet befejezéséről az OpenID szolgáltatónál.", + "PERMISSIONCHECKV2": "Engedély ellenőrzés V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Ha a zászló engedélyezve van, használhatja az új API-t és annak funkcióit.", + "WEBKEY": "Webkulcs", + "WEBKEY_DESCRIPTION": "Ha a zászló engedélyezve van, használhatja az új API-t és annak funkcióit.", "STATES": { "INHERITED": "Örököl", "ENABLED": "Engedélyezve", @@ -1484,7 +1648,12 @@ "ENABLED": "\"Engedélyezve\" öröklődik", "DISABLED": "\"Letiltva\" öröklődik" }, - "RESET": "Mindent állíts öröklésre" + "RESET": "Mindent állíts öröklésre", + "CONSOLEUSEV2USERAPI": "Használja a V2 API-t a konzolban felhasználók létrehozásához", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Ha ez a jelző engedélyezve van, a konzol a V2 User API-t használja új felhasználók létrehozásához. A V2 API-val az újonnan létrehozott felhasználók kezdeti állapot nélkül indulnak.", + "LOGINV2": "Bejelentkezés V2", + "LOGINV2_DESCRIPTION": "Ennek engedélyezésével aktiválódik az új, TypeScript-alapú bejelentkezési felület, amely jobb biztonságot, teljesítményt és testreszabhatóságot nyújt.", + "LOGINV2_BASEURI": "Alap URI" }, "DIALOG": { "RESET": { @@ -1608,9 +1777,9 @@ "de": "Deutsch", "en": "English", "es": "Español", - "fr": "Francia", - "it": "Olasz", - "ja": "Japán", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", "pl": "Lengyel", "zh": "Egyszerűsített kínai", "bg": "Bolgár", @@ -1621,7 +1790,9 @@ "nl": "Holland", "sv": "Svéd", "id": "Indonéz", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "E-mail ellenőrzés kész", @@ -2210,7 +2381,9 @@ "REMOVED": "Sikeresen eltávolítva." }, "ISIDTOKENMAPPING": "Hozzárendelés az ID token alapján", - "ISIDTOKENMAPPING_DESC": "Ha ezt választod, a szolgáltatói információkat az ID token alapján rendeljük hozzá, nem a userinfo végpontból." + "ISIDTOKENMAPPING_DESC": "Ha ezt választod, a szolgáltatói információkat az ID token alapján rendeljük hozzá, nem a userinfo végpontból.", + "USEPKCE": "PKCE használata", + "USEPKCE_DESC": "Meghatározza, hogy a code_challenge és a code_challenge_method paraméterek szerepeljenek-e a hitelesítési kérelemben" }, "MFA": { "LIST": { @@ -2577,12 +2750,12 @@ "3": "Egyéb" }, "LANGUAGES": { - "de": "Német", - "en": "Angol", - "es": "Spanyol", - "fr": "Francia", - "it": "Olasz", - "ja": "Japán", + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", "pl": "Lengyel", "zh": "Egyszerűsített kínai", "bg": "Bolgár", @@ -2593,7 +2766,9 @@ "nl": "Holland", "sv": "Svéd", "id": "Indonéz", - "hu": "Magyar" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Hozzáadás egy menedzsert", diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index d5dbd48128..77584e883b 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -173,6 +173,32 @@ "DESCRIPTION": "Masa pakai token penyegaran yang menganggur adalah waktu maksimum token penyegaran tidak dapat digunakan." } }, + "WEB_KEYS": { + "DESCRIPTION": "Kelola Kunci Web OIDC Anda untuk menandatangani dan memvalidasi token dengan aman untuk instance ZITADEL Anda.", + "TABLE": { + "TITLE": "Kunci Web Aktif dan Mendatang", + "DESCRIPTION": "Kunci web Anda yang aktif dan akan datang. Mengaktifkan kunci baru akan menonaktifkan kunci yang sedang digunakan.", + "NOTE": "Catatan: Endpoint JWKs OIDC mengembalikan respons yang dapat di-cache (default 5 menit). Hindari mengaktifkan kunci terlalu cepat, karena mungkin belum tersedia di cache dan klien.", + "ACTIVATE": "Aktifkan Kunci Web Berikutnya", + "ACTIVE": "Saat ini aktif", + "NEXT": "Berikutnya dalam antrean", + "FUTURE": "Mendatang", + "WARNING": "Kunci Web berusia kurang dari 5 menit" + }, + "CREATE": { + "TITLE": "Buat Kunci Web Baru", + "DESCRIPTION": "Membuat kunci web baru akan menambahkannya ke daftar Anda. ZITADEL secara default menggunakan kunci RSA2048 dengan fungsi hash SHA256.", + "KEY_TYPE": "Jenis Kunci", + "BITS": "Bit", + "HASHER": "Hasher", + "CURVE": "Kurva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Kunci Web Sebelumnya", + "DESCRIPTION": "Ini adalah kunci web sebelumnya yang tidak lagi aktif.", + "DEACTIVATED_ON": "Dinonaktifkan pada" + } + }, "MESSAGE_TEXTS": { "TITLE": "Teks Pesan", "DESCRIPTION": "Sesuaikan teks email notifikasi atau pesan SMS Anda. Jika Anda ingin menonaktifkan beberapa bahasa, batasi bahasa tersebut di pengaturan bahasa instance Anda.", @@ -469,6 +495,115 @@ "DOWNLOAD": "Unduh", "APPLY": "Menerapkan" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Tindakan", + "DESCRIPTION": "Tindakan memungkinkan Anda menjalankan kode khusus sebagai respons terhadap permintaan API, peristiwa, atau fungsi tertentu. Gunakan ini untuk memperluas Zitadel, mengotomatiskan alur kerja, dan berintegrasi dengan sistem lain.", + "TYPES": { + "request": "Permintaan", + "response": "Respons", + "events": "Peristiwa", + "function": "Fungsi" + }, + "DIALOG": { + "CREATE_TITLE": "Buat Tindakan", + "UPDATE_TITLE": "Perbarui Tindakan", + "TYPE": { + "DESCRIPTION": "Pilih kapan Anda ingin Tindakan ini dijalankan", + "REQUEST": { + "TITLE": "Permintaan", + "DESCRIPTION": "Permintaan yang terjadi di dalam Zitadel. Ini bisa berupa sesuatu seperti panggilan permintaan login." + }, + "RESPONSE": { + "TITLE": "Respons", + "DESCRIPTION": "Respons dari permintaan di dalam Zitadel. Pikirkan respons yang Anda dapatkan kembali dari pengambilan pengguna." + }, + "EVENTS": { + "TITLE": "Peristiwa", + "DESCRIPTION": "Peristiwa yang terjadi di dalam Zitadel. Ini bisa berupa apa saja seperti pengguna membuat akun, login berhasil, dll." + }, + "FUNCTIONS": { + "TITLE": "Fungsi", + "DESCRIPTION": "Fungsi yang dapat Anda panggil di dalam Zitadel. Ini bisa berupa apa saja mulai dari mengirim email hingga membuat pengguna." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Pilih apakah tindakan ini berlaku untuk semua permintaan, layanan tertentu (mis. manajemen pengguna), atau permintaan tunggal (mis. buat pengguna).", + "ALL": { + "TITLE": "Semua", + "DESCRIPTION": "Pilih ini jika Anda ingin menjalankan tindakan Anda pada setiap permintaan" + }, + "SELECT_SERVICE": { + "TITLE": "Pilih Layanan", + "DESCRIPTION": "Pilih Layanan Zitadel untuk tindakan Anda." + }, + "SELECT_METHOD": { + "TITLE": "Pilih Metode", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada permintaan tertentu, pilih di sini", + "NOTE": "Jika Anda tidak memilih metode, tindakan Anda akan berjalan pada setiap permintaan di layanan yang Anda pilih." + }, + "FUNCTIONNAME": { + "TITLE": "Nama Fungsi", + "DESCRIPTION": "Pilih fungsi yang ingin Anda jalankan" + }, + "SELECT_GROUP": { + "TITLE": "Tetapkan Grup", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada grup peristiwa, tetapkan grup di sini" + }, + "SELECT_EVENT": { + "TITLE": "Pilih Peristiwa", + "DESCRIPTION": "Jika Anda hanya ingin menjalankan pada peristiwa tertentu, tentukan di sini" + } + }, + "TARGET": { + "DESCRIPTION": "Anda dapat memilih untuk menjalankan target, atau menjalankannya dengan kondisi yang sama dengan target lain.", + "TARGET": { + "DESCRIPTION": "Target yang ingin Anda jalankan untuk tindakan ini" + }, + "CONDITIONS": { + "DESCRIPTION": "Kondisi Eksekusi" + } + } + }, + "TABLE": { + "CONDITION": "Kondisi", + "TYPE": "Jenis", + "TARGET": "Target", + "CREATIONDATE": "Tanggal Pembuatan" + } + }, + "TARGET": { + "TITLE": "Target", + "DESCRIPTION": "Target adalah tujuan kode yang ingin Anda jalankan dari tindakan. Buat target di sini dan tambahkan ke tindakan Anda.", + "CREATE": { + "TITLE": "Buat Target Anda", + "DESCRIPTION": "Buat target Anda sendiri di luar Zitadel", + "NAME": "Nama", + "NAME_DESCRIPTION": "Beri target Anda nama yang jelas dan deskriptif agar mudah diidentifikasi nanti", + "TYPE": "Jenis", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Panggilan REST", + "restAsync": "REST Asinkron" + }, + "ENDPOINT": "Titik Akhir", + "ENDPOINT_DESCRIPTION": "Masukkan titik akhir tempat kode Anda dihosting. Pastikan dapat diakses oleh kami!", + "TIMEOUT": "Batas Waktu", + "TIMEOUT_DESCRIPTION": "Tetapkan waktu maksimum target Anda untuk merespons. Jika membutuhkan waktu lebih lama, kami akan menghentikan permintaan.", + "INTERRUPT_ON_ERROR": "Interupsi jika Terjadi Kesalahan", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Hentikan semua eksekusi saat target mengembalikan kesalahan", + "INTERRUPT_ON_ERROR_WARNING": "Perhatian: \"Interupsi jika Terjadi Kesalahan\" akan menghentikan operasi jika terjadi kegagalan, berisiko mengunci akses. Uji dengan opsi ini dinonaktifkan untuk mencegah pemblokiran login/pembuatan.", + "AWAIT_RESPONSE": "Tunggu Respons", + "AWAIT_RESPONSE_DESCRIPTION": "Kami akan menunggu respons sebelum melakukan hal lain. Berguna jika Anda berniat menggunakan beberapa target untuk satu tindakan" + }, + "TABLE": { + "NAME": "Nama", + "ENDPOINT": "Titik Akhir", + "CREATIONDATE": "Tanggal Pembuatan", + "REORDER": "Susun ulang" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Memiliki kendali atas seluruh instansi, termasuk semua organisasi", "IAM_OWNER_VIEWER": "Memiliki izin untuk meninjau seluruh instansi, termasuk semua organisasi", @@ -476,14 +611,17 @@ "IAM_USER_MANAGER": "Memiliki izin untuk membuat dan mengelola pengguna", "IAM_ADMIN_IMPERSONATOR": "Memiliki izin untuk menyamar sebagai admin dan pengguna akhir dari semua organisasi", "IAM_END_USER_IMPERSONATOR": "Memiliki izin untuk meniru identitas pengguna akhir dari semua organisasi", + "IAM_LOGIN_CLIENT": "Memiliki izin untuk mengelola klien masuk", "ORG_OWNER": "Memiliki izin atas seluruh organisasi", "ORG_USER_MANAGER": "Memiliki izin untuk membuat dan mengelola pengguna organisasi", "ORG_OWNER_VIEWER": "Memiliki izin untuk meninjau seluruh organisasi", + "ORG_SETTINGS_MANAGER": "Memiliki izin untuk mengelola pengaturan organisasi", "ORG_USER_PERMISSION_EDITOR": "Memiliki izin untuk mengelola izin pengguna", "ORG_PROJECT_PERMISSION_EDITOR": "Memiliki izin untuk mengelola hibah proyek", "ORG_PROJECT_CREATOR": "Memiliki izin untuk membuat proyeknya sendiri dan pengaturan yang mendasarinya", "ORG_ADMIN_IMPERSONATOR": "Memiliki izin untuk menyamar sebagai admin dan pengguna akhir dari organisasi", "ORG_END_USER_IMPERSONATOR": "Memiliki izin untuk meniru identitas pengguna akhir dari organisasi", + "ORG_USER_SELF_MANAGER": "Memiliki izin untuk mengelola pengguna sendiri", "PROJECT_OWNER": "Memiliki izin atas keseluruhan proyek", "PROJECT_OWNER_VIEWER": "Memiliki izin untuk meninjau keseluruhan proyek", "PROJECT_OWNER_GLOBAL": "Memiliki izin atas keseluruhan proyek", @@ -727,7 +865,10 @@ "PHONESECTION": "Nomor telepon", "PASSWORDSECTION": "Kata Sandi Awal", "ADDRESSANDPHONESECTION": "Nomor telepon", - "INITMAILDESCRIPTION": "Jika kedua opsi dipilih, tidak ada email untuk inisialisasi yang akan dikirim. Jika hanya salah satu opsi yang dipilih, email untuk menyediakan/memverifikasi data akan dikirim." + "INITMAILDESCRIPTION": "Jika kedua opsi dipilih, tidak ada email untuk inisialisasi yang akan dikirim. Jika hanya salah satu opsi yang dipilih, email untuk menyediakan/memverifikasi data akan dikirim.", + "SETUPAUTHENTICATIONLATER": "Atur autentikasi nanti untuk pengguna ini.", + "INVITATION": "Kirim Email undangan untuk pengaturan autentikasi dan verifikasi Email.", + "INITIALPASSWORD": "Tetapkan kata sandi awal untuk pengguna." }, "CODEDIALOG": { "TITLE": "Verifikasi Nomor Telepon", @@ -749,6 +890,9 @@ "EMAIL": "E-mail", "PHONE": "Nomor telepon", "PHONE_HINT": "Gunakan simbol + diikuti dengan kode negara pemanggil, atau pilih negara dari dropdown dan terakhir masukkan nomor telepon", + "PHONE_VERIFIED": "Nomor telepon terverifikasi", + "SEND_SMS": "Kirim SMS verifikasi", + "SEND_EMAIL": "Kirim E-mail", "USERNAME": "Nama belakang", "CHANGEUSERNAME": "memodifikasi", "CHANGEUSERNAME_TITLE": "Ubah nama pengguna", @@ -878,6 +1022,14 @@ "5": "Tergantung", "6": "Awal" }, + "STATEV2": { + "0": "Tidak dikenal", + "1": "Aktif", + "2": "Tidak aktif", + "3": "Dihapus", + "4": "Terkunci", + "5": "Awal" + }, "SEARCH": { "ADDITIONAL": "Nama login (organisasi saat ini)", "ADDITIONAL-EXTERNAL": "Nama login (organisasi eksternal)" @@ -1218,6 +1370,7 @@ "BRANDING": "merek", "PRIVACYPOLICY": "Tautan eksternal", "OIDC": "Masa berlaku dan masa berlaku Token OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Pembuat Rahasia", "SECURITY": "Pengaturan keamanan", "EVENTS": "Acara", @@ -1263,7 +1416,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1348,13 +1502,27 @@ "USERSCHEMA_DESCRIPTION": "Skema Pengguna memungkinkan untuk mengelola skema data pengguna. Jika tanda ini diaktifkan, Anda akan dapat menggunakan API baru dan fitur-fiturnya.", "ACTIONS": "Tindakan", "ACTIONS_DESCRIPTION": "Tindakan v2 memungkinkan untuk mengelola eksekusi dan target data. Jika tanda ini diaktifkan, Anda akan dapat menggunakan API baru dan fitur-fiturnya.", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "If the flag is enabled, the OIDC parent error will be logged in the console.", + "DISABLEUSERTOKENEVENT": "Disable User Token Event", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Enable Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "The Back-Channel Logout implements OpenID Connect Back-Channel Logout 1.0 and can be used to notify clients about session termination at the OpenID Provider.", + "PERMISSIONCHECKV2": "Permission Check V2", + "PERMISSIONCHECKV2_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", + "WEBKEY": "Web Key", + "WEBKEY_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", "STATES": { "INHERITED": "Mewarisi", "ENABLED": "Diaktifkan", "DISABLED": "Dengan disabilitas" }, "INHERITED_DESCRIPTION": "Ini menetapkan nilai ke nilai default sistem.", "INHERITEDINDICATOR_DESCRIPTION": { "ENABLED": "\"Diaktifkan\" diwariskan", "DISABLED": "\"Dinonaktifkan\" diwariskan" }, - "RESET": "Tetapkan semua untuk diwarisi" + "RESET": "Tetapkan semua untuk diwarisi", + "CONSOLEUSEV2USERAPI": "Gunakan API V2 di konsol untuk pembuatan pengguna", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Ketika flag ini diaktifkan, konsol menggunakan API Pengguna V2 untuk membuat pengguna baru. Dengan API V2, pengguna yang baru dibuat dimulai tanpa keadaan awal.", + "LOGINV2": "Login V2", + "LOGINV2_DESCRIPTION": "Mengaktifkan ini akan mengaktifkan antarmuka login baru berbasis TypeScript dengan keamanan, performa, dan kustomisasi yang lebih baik.", + "LOGINV2_BASEURI": "URI dasar" }, "DIALOG": { "RESET": { @@ -1488,7 +1656,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Verifikasi email selesai", @@ -1993,7 +2163,9 @@ "REMOVED": "Berhasil dihapus." }, "ISIDTOKENMAPPING": "Peta dari token ID", - "ISIDTOKENMAPPING_DESC": "Jika dipilih, informasi penyedia akan dipetakan dari token ID, bukan dari titik akhir info pengguna." + "ISIDTOKENMAPPING_DESC": "Jika dipilih, informasi penyedia akan dipetakan dari token ID, bukan dari titik akhir info pengguna.", + "USEPKCE": "Gunakan PKCE", + "USEPKCE_DESC": "Menentukan apakah parameter code_challenge dan code_challenge_method disertakan dalam permintaan autentikasi" }, "MFA": { "LIST": { @@ -2279,7 +2451,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Tambahkan Manajer", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 1910146698..b01762b175 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -185,6 +185,32 @@ "DESCRIPTION": "La durata massima di un token di refresh inattivo è il tempo massimo in cui un token di refresh può rimanere inutilizzato." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gestisci le tue chiavi Web OIDC per firmare e convalidare in modo sicuro i token per la tua istanza di ZITADEL.", + "TABLE": { + "TITLE": "Chiavi Web Attive e Future", + "DESCRIPTION": "Le tue chiavi web attive e future. L'attivazione di una nuova chiave disattiverà quella attuale.", + "NOTE": "Nota: L'endpoint JWKs OIDC restituisce una risposta memorizzabile nella cache (predefinito 5 min). Evita di attivare una chiave troppo presto, poiché potrebbe non essere ancora disponibile nelle cache e nei client.", + "ACTIVATE": "Attiva la prossima Chiave Web", + "ACTIVE": "Attualmente attiva", + "NEXT": "Prossima in coda", + "FUTURE": "Futura", + "WARNING": "La chiave web ha meno di 5 minuti" + }, + "CREATE": { + "TITLE": "Crea una nuova Chiave Web", + "DESCRIPTION": "Creare una nuova chiave web la aggiungerà alla tua lista. ZITADEL utilizza chiavi RSA2048 con hash SHA256 per impostazione predefinita.", + "KEY_TYPE": "Tipo di Chiave", + "BITS": "Bit", + "HASHER": "Hasher", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Chiavi Web Precedenti", + "DESCRIPTION": "Queste sono le tue chiavi web precedenti che non sono più attive.", + "DEACTIVATED_ON": "Disattivata il" + } + }, "MESSAGE_TEXTS": { "TITLE": "Testi dei Messaggi", "DESCRIPTION": "Personalizza i testi delle tue email di notifica o messaggi SMS. Se vuoi disabilitare alcune lingue, limitale nelle impostazioni lingua delle tue istanze.", @@ -501,6 +527,115 @@ "DOWNLOAD": "Scarica", "APPLY": "Applicare" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Azioni", + "DESCRIPTION": "Le azioni consentono di eseguire codice personalizzato in risposta a richieste API, eventi o funzioni specifiche. Usale per estendere Zitadel, automatizzare i flussi di lavoro e integrarti con altri sistemi.", + "TYPES": { + "request": "Richiesta", + "response": "Risposta", + "events": "Eventi", + "function": "Funzione" + }, + "DIALOG": { + "CREATE_TITLE": "Crea un'azione", + "UPDATE_TITLE": "Aggiorna un'azione", + "TYPE": { + "DESCRIPTION": "Seleziona quando vuoi che venga eseguita questa azione", + "REQUEST": { + "TITLE": "Richiesta", + "DESCRIPTION": "Richieste che si verificano all'interno di Zitadel. Potrebbe trattarsi di una chiamata di richiesta di accesso." + }, + "RESPONSE": { + "TITLE": "Risposta", + "DESCRIPTION": "Una risposta a una richiesta all'interno di Zitadel. Pensa alla risposta che ricevi quando recuperi un utente." + }, + "EVENTS": { + "TITLE": "Eventi", + "DESCRIPTION": "Eventi che si verificano all'interno di Zitadel. Potrebbe trattarsi di qualsiasi cosa, come un utente che crea un account, un accesso riuscito, ecc." + }, + "FUNCTIONS": { + "TITLE": "Funzioni", + "DESCRIPTION": "Funzioni che puoi chiamare all'interno di Zitadel. Potrebbe trattarsi di qualsiasi cosa, dall'invio di un'e-mail alla creazione di un utente." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Scegli se questa azione si applica a tutte le richieste, a un servizio specifico (ad es. gestione utenti) o a una singola richiesta (ad es. crea utente).", + "ALL": { + "TITLE": "Tutte", + "DESCRIPTION": "Seleziona questa opzione se vuoi eseguire la tua azione su ogni richiesta" + }, + "SELECT_SERVICE": { + "TITLE": "Seleziona servizio", + "DESCRIPTION": "Scegli un servizio Zitadel per la tua azione." + }, + "SELECT_METHOD": { + "TITLE": "Seleziona metodo", + "DESCRIPTION": "Se vuoi eseguire solo su una richiesta specifica, selezionala qui", + "NOTE": "Se non selezioni un metodo, la tua azione verrà eseguita su ogni richiesta nel servizio selezionato." + }, + "FUNCTIONNAME": { + "TITLE": "Nome funzione", + "DESCRIPTION": "Scegli la funzione che vuoi eseguire" + }, + "SELECT_GROUP": { + "TITLE": "Imposta gruppo", + "DESCRIPTION": "Se vuoi eseguire solo su un gruppo di eventi, imposta il gruppo qui" + }, + "SELECT_EVENT": { + "TITLE": "Seleziona evento", + "DESCRIPTION": "Se vuoi eseguire solo su un evento specifico, specificalo qui" + } + }, + "TARGET": { + "DESCRIPTION": "Puoi scegliere di eseguire un obiettivo o di eseguirlo alle stesse condizioni di altri obiettivi.", + "TARGET": { + "DESCRIPTION": "L'obiettivo che vuoi eseguire per questa azione" + }, + "CONDITIONS": { + "DESCRIPTION": "Condizioni di esecuzione" + } + } + }, + "TABLE": { + "CONDITION": "Condizione", + "TYPE": "Tipo", + "TARGET": "Obiettivo", + "CREATIONDATE": "Data di creazione" + } + }, + "TARGET": { + "TITLE": "Obiettivi", + "DESCRIPTION": "Un obiettivo è la destinazione del codice che vuoi eseguire da un'azione. Crea un obiettivo qui e aggiungilo alle tue azioni.", + "CREATE": { + "TITLE": "Crea il tuo obiettivo", + "DESCRIPTION": "Crea il tuo obiettivo al di fuori di Zitadel", + "NAME": "Nome", + "NAME_DESCRIPTION": "Dai al tuo obiettivo un nome chiaro e descrittivo per renderlo facile da identificare in seguito", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Chiamata REST", + "restAsync": "REST Asincrono" + }, + "ENDPOINT": "Endpoint", + "ENDPOINT_DESCRIPTION": "Inserisci l'endpoint in cui è ospitato il tuo codice. Assicurati che sia accessibile per noi!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Imposta il tempo massimo che il tuo obiettivo ha per rispondere. Se impiega più tempo, interromperemo la richiesta.", + "INTERRUPT_ON_ERROR": "Interrompi in caso di errore", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Interrompi tutte le esecuzioni quando gli obiettivi restituiscono un errore", + "INTERRUPT_ON_ERROR_WARNING": "Attenzione: “Interrompi in caso di errore” arresta le operazioni in caso di fallimento, rischiando il blocco. Testare con l’opzione disattivata per evitare il blocco dell’accesso/creazione.", + "AWAIT_RESPONSE": "Attendi risposta", + "AWAIT_RESPONSE_DESCRIPTION": "Aspetteremo una risposta prima di fare altro. Utile se intendi utilizzare più obiettivi per una singola azione" + }, + "TABLE": { + "NAME": "Nome", + "ENDPOINT": "Endpoint", + "CREATIONDATE": "Data di creazione", + "REORDER": "Riordina" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Ha il controllo sull'intera istanza, comprese tutte le organizzazioni", "IAM_OWNER_VIEWER": "Ha l'autorizzazione per esaminare l'intera istanza, comprese tutte le organizzazioni", @@ -508,14 +643,17 @@ "IAM_USER_MANAGER": "Ha l'autorizzazione per creare e gestire utenti", "IAM_ADMIN_IMPERSONATOR": "Dispone dell'autorizzazione per rappresentare l'amministratore e gli utenti finali di tutte le organizzazioni", "IAM_END_USER_IMPERSONATOR": "Dispone dell'autorizzazione per rappresentare gli utenti finali di tutte le organizzazioni", + "IAM_LOGIN_CLIENT": "Ha il permesso di gestire i client di accesso", "ORG_OWNER": "Ha il permesso su tutta l'organizzazione", "ORG_USER_MANAGER": "Ha l'autorizzazione per creare e gestire gli utenti dell'organizzazione", "ORG_OWNER_VIEWER": "Ha il permesso di esaminare l'intera organizzazione", + "ORG_SETTINGS_MANAGER": "Ha il permesso di gestire le impostazioni dell'organizzazione", "ORG_USER_PERMISSION_EDITOR": "Ha l'autorizzazione per gestire le autorizzazioni degli utenti", "ORG_PROJECT_PERMISSION_EDITOR": "Ha il permesso di gestire le sovvenzioni di progetto (Project Grant)", "ORG_PROJECT_CREATOR": "Ha il permesso di creare propri progetti e le impostazioni sottostanti", "ORG_ADMIN_IMPERSONATOR": "Ha il permesso per rappresentare l'amministratore e gli utenti finali dell'organizzazione", "ORG_END_USER_IMPERSONATOR": "Ha il permesso per rappresentare gli utenti finali dell'organizzazione", + "ORG_USER_SELF_MANAGER": "Ha il permesso per gestire il proprio account utente", "PROJECT_OWNER": "Ha il permesso per l'intero progetto", "PROJECT_OWNER_VIEWER": "Ha il permesso di esaminare l'intero progetto", "PROJECT_OWNER_GLOBAL": "Ha il permesso per l'intero progetto", @@ -786,7 +924,10 @@ "PHONESECTION": "Phone numbers", "PASSWORDSECTION": "Password iniziale", "ADDRESSANDPHONESECTION": "Numero di telefono", - "INITMAILDESCRIPTION": "Se vengono selezionate entrambe le opzioni, non verrà inviata alcuna e-mail per l'inizializzazione. Se solo una delle opzioni viene selezionata, verrà inviata una mail per fornire/verificare i dati." + "INITMAILDESCRIPTION": "Se vengono selezionate entrambe le opzioni, non verrà inviata alcuna e-mail per l'inizializzazione. Se solo una delle opzioni viene selezionata, verrà inviata una mail per fornire/verificare i dati.", + "SETUPAUTHENTICATIONLATER": "Configura l'autenticazione più tardi per questo utente.", + "INVITATION": "Invia un'e-mail di invito per la configurazione dell'autenticazione e la verifica dell'e-mail.", + "INITIALPASSWORD": "Imposta una password iniziale per l'utente." }, "CODEDIALOG": { "TITLE": "Verificare il numero di telefono", @@ -808,6 +949,9 @@ "EMAIL": "E-mail", "PHONE": "Numero di telefono", "PHONE_HINT": "Utilizza il simbolo + seguito dal prefisso del paese, o seleziona il paese ed inserisci il numero di telefono", + "PHONE_VERIFIED": "Numero di telefono verificato", + "SEND_SMS": "Invia SMS di verifica", + "SEND_EMAIL": "Invia E-mail", "USERNAME": "Nome utente", "CHANGEUSERNAME": "cambia", "CHANGEUSERNAME_TITLE": "Cambia nome utente", @@ -948,6 +1092,14 @@ "5": "Sospeso", "6": "Initializzato" }, + "STATEV2": { + "0": "Sconosciuto", + "1": "Attivo", + "2": "Inattivo", + "3": "Rimosso", + "4": "Bloccato", + "5": "Initializzato" + }, "SEARCH": { "ADDITIONAL": "Nome (organizzazione corrente)", "ADDITIONAL-EXTERNAL": "Loginname (organizzazione esterna)" @@ -1340,6 +1492,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Informativa sulla privacy e TOS", "OIDC": "OIDC Token lifetime e scadenza", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Aspetto dei segreti", "SECURITY": "Impostazioni di sicurezza", "EVENTS": "Eventi", @@ -1385,7 +1538,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1476,6 +1630,16 @@ "ACTIONS_DESCRIPTION": "Le azioni v2 consentono di gestire le esecuzioni e gli obiettivi dei dati. Se l'indicatore è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Terminazione sessione", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Se il flag è abilitato, sarai in grado di terminare una singola sessione dall'interfaccia utente di accesso fornendo un id_token con una richiesta `sid` come id_token_hint nel punto finale di end_session. Tieni presente che attualmente tutte le sessioni dello stesso agente utente (browser) vengono terminate nell'interfaccia utente di accesso. Le sessioni gestite tramite l'API di sessione consentono già la terminazione di singole sessioni.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Parent Error", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Se il flag è abilitato, l'errore del genitore OIDC verrà registrato nella console.", + "DISABLEUSERTOKENEVENT": "Disabilita evento token utente", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Abilita Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Il Back-Channel Logout implementa OpenID Connect Back-Channel Logout 1.0 e può essere utilizzato per notificare ai client la terminazione della sessione presso il provider OpenID.", + "PERMISSIONCHECKV2": "Controllo permessi V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Se il flag è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", + "WEBKEY": "Chiave Web", + "WEBKEY_DESCRIPTION": "Se il flag è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", "STATES": { "INHERITED": "Predefinito", "ENABLED": "Abilitato", @@ -1486,7 +1650,12 @@ "ENABLED": "\"Abilitato\" viene ereditato", "DISABLED": "\"Disabilitato\" viene ereditato" }, - "RESET": "Imposta tutto su predefinito" + "RESET": "Imposta tutto su predefinito", + "CONSOLEUSEV2USERAPI": "Utilizza l'API V2 nella console per la creazione degli utenti", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Quando questa opzione è abilitata, la console utilizza l'API V2 User per creare nuovi utenti. Con l'API V2, i nuovi utenti creati iniziano senza uno stato iniziale.", + "LOGINV2": "Accesso V2", + "LOGINV2_DESCRIPTION": "Abilitando questa opzione si attiva la nuova interfaccia di login basata su TypeScript con sicurezza, prestazioni e personalizzazione migliorate.", + "LOGINV2_BASEURI": "URI di base" }, "DIALOG": { "RESET": { @@ -1623,7 +1792,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Verifica dell'e-mail terminata con successo.", @@ -2192,7 +2363,9 @@ "REMOVED": "Rimosso con successo." }, "ISIDTOKENMAPPING": "Mappatura dal token ID", - "ISIDTOKENMAPPING_DESC": "Se selezionato, le informazioni del provider vengono mappate dal token ID, non dal punto finale userinfo." + "ISIDTOKENMAPPING_DESC": "Se selezionato, le informazioni del provider vengono mappate dal token ID, non dal punto finale userinfo.", + "USEPKCE": "Usa PKCE", + "USEPKCE_DESC": "Determina se i parametri code_challenge e code_challenge_method sono inclusi nella richiesta di autenticazione" }, "MFA": { "LIST": { @@ -2571,7 +2744,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Aggiungi un manager", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 3e75cd94a2..c0429ec816 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -185,6 +185,32 @@ "DESCRIPTION": "アイドル状態のリフレッシュトークンの有効期間は、リフレッシュトークンが使用されない最大時間です。" } }, + "WEB_KEYS": { + "DESCRIPTION": "ZITADELインスタンスのトークンを安全に署名および検証するために、OIDC Webキーを管理します。", + "TABLE": { + "TITLE": "アクティブおよび今後のWebキー", + "DESCRIPTION": "現在アクティブなWebキーと、今後使用予定のWebキーです。新しいキーをアクティブ化すると、現在のキーは無効になります。", + "NOTE": "注意: JWKs OIDCエンドポイントはキャッシュ可能なレスポンスを返します(デフォルト5分)。キーを早くアクティブ化しすぎると、キャッシュやクライアントでまだ利用できない可能性があります。", + "ACTIVATE": "次のWebキーをアクティブ化", + "ACTIVE": "現在アクティブ", + "NEXT": "次のキュー", + "FUTURE": "今後", + "WARNING": "ウェブキーは5分未満です。" + }, + "CREATE": { + "TITLE": "新しいWebキーを作成", + "DESCRIPTION": "新しいWebキーを作成すると、リストに追加されます。ZITADELはデフォルトでRSA2048キーとSHA256ハッシュを使用します。", + "KEY_TYPE": "キーの種類", + "BITS": "ビット", + "HASHER": "ハッシュ方式", + "CURVE": "カーブ" + }, + "PREVIOUS_TABLE": { + "TITLE": "以前のWebキー", + "DESCRIPTION": "これらは、すでに無効になった以前のWebキーです。", + "DEACTIVATED_ON": "無効化日" + } + }, "MESSAGE_TEXTS": { "TITLE": "メッセージテキスト", "DESCRIPTION": "通知メールやSMSメッセージのテキストをカスタマイズします。一部の言語を無効にしたい場合は、インスタンスの言語設定で制限してください。", @@ -502,6 +528,115 @@ "DOWNLOAD": "ダウンロード", "APPLY": "アプライ" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "アクション", + "DESCRIPTION": "アクションを使用すると、APIリクエスト、イベント、または特定の関数に応答してカスタムコードを実行できます。これらを使用して、Zitadelを拡張し、ワークフローを自動化し、他のシステムと統合します。", + "TYPES": { + "request": "リクエスト", + "response": "レスポンス", + "events": "イベント", + "function": "関数" + }, + "DIALOG": { + "CREATE_TITLE": "アクションを作成", + "UPDATE_TITLE": "アクションを更新", + "TYPE": { + "DESCRIPTION": "このアクションを実行するタイミングを選択します", + "REQUEST": { + "TITLE": "リクエスト", + "DESCRIPTION": "Zitadel内で発生するリクエスト。これはログインリクエストの呼び出しのようなものです。" + }, + "RESPONSE": { + "TITLE": "レスポンス", + "DESCRIPTION": "Zitadel内のリクエストからのレスポンス。ユーザーのフェッチから返されるレスポンスを考えてください。" + }, + "EVENTS": { + "TITLE": "イベント", + "DESCRIPTION": "Zitadel内で発生するイベント。これは、ユーザーアカウントの作成、ログインの成功など、あらゆる可能性があります。" + }, + "FUNCTIONS": { + "TITLE": "関数", + "DESCRIPTION": "Zitadel内で呼び出すことができる関数。これは、電子メールの送信からユーザーの作成まで、あらゆる可能性があります。" + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "このアクションをすべてのリクエスト、特定のサービス(例:ユーザー管理)、または単一のリクエスト(例:ユーザーの作成)に適用するかどうかを選択します。", + "ALL": { + "TITLE": "すべて", + "DESCRIPTION": "すべてのリクエストでアクションを実行する場合は、これを選択します" + }, + "SELECT_SERVICE": { + "TITLE": "サービスを選択", + "DESCRIPTION": "アクションのZitadelサービスを選択します。" + }, + "SELECT_METHOD": { + "TITLE": "メソッドを選択", + "DESCRIPTION": "特定のリクエストでのみ実行する場合は、ここで選択します", + "NOTE": "メソッドを選択しない場合、アクションは選択したサービスのすべてのリクエストで実行されます。" + }, + "FUNCTIONNAME": { + "TITLE": "関数名", + "DESCRIPTION": "実行する関数を選択します" + }, + "SELECT_GROUP": { + "TITLE": "グループを設定", + "DESCRIPTION": "イベントのグループでのみ実行する場合は、ここでグループを設定します" + }, + "SELECT_EVENT": { + "TITLE": "イベントを選択", + "DESCRIPTION": "特定のイベントでのみ実行する場合は、ここで指定します" + } + }, + "TARGET": { + "DESCRIPTION": "ターゲットを実行するか、他のターゲットと同じ条件で実行するかを選択できます。", + "TARGET": { + "DESCRIPTION": "このアクションで実行するターゲット" + }, + "CONDITIONS": { + "DESCRIPTION": "実行条件" + } + } + }, + "TABLE": { + "CONDITION": "条件", + "TYPE": "タイプ", + "TARGET": "ターゲット", + "CREATIONDATE": "作成日" + } + }, + "TARGET": { + "TITLE": "ターゲット", + "DESCRIPTION": "ターゲットは、アクションから実行するコードの宛先です。ここでターゲットを作成し、アクションに追加します。", + "CREATE": { + "TITLE": "ターゲットを作成", + "DESCRIPTION": "Zitadelの外部で独自のターゲットを作成します", + "NAME": "名前", + "NAME_DESCRIPTION": "後で簡単に識別できるように、ターゲットに明確でわかりやすい名前を付けます", + "TYPE": "タイプ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST 呼び出し", + "restAsync": "REST 非同期" + }, + "ENDPOINT": "エンドポイント", + "ENDPOINT_DESCRIPTION": "コードがホストされているエンドポイントを入力します。アクセス可能であることを確認してください。", + "TIMEOUT": "タイムアウト", + "TIMEOUT_DESCRIPTION": "ターゲットが応答する最大時間を設定します。これより時間がかかる場合は、リクエストを停止します。", + "INTERRUPT_ON_ERROR": "エラー時に中断", + "INTERRUPT_ON_ERROR_DESCRIPTION": "ターゲットがエラーを返した場合、すべての実行を停止します", + "INTERRUPT_ON_ERROR_WARNING": "注意:「エラー時に中断」を有効にすると、失敗時に処理が停止し、ロックアウトのリスクがあります。ログイン/作成のブロックを防ぐため、無効にしてテストしてください。", + "AWAIT_RESPONSE": "レスポンスを待機", + "AWAIT_RESPONSE_DESCRIPTION": "他の処理を行う前にレスポンスを待機します。単一のアクションに複数のターゲットを使用する場合に便利です" + }, + "TABLE": { + "NAME": "名前", + "ENDPOINT": "エンドポイント", + "CREATIONDATE": "作成日", + "REORDER": "順序を変更" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "すべての組織を含むインスタンス全体を管理する権限を持ちます", "IAM_OWNER_VIEWER": "すべての組織を含むインスタンス全体を閲覧する権限を持ちます", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "ユーザーの作成および管理する権限を持ちます", "IAM_ADMIN_IMPERSONATOR": "すべての組織の管理者およびエンドユーザーになりすます権限を持っています", "IAM_END_USER_IMPERSONATOR": "すべての組織のエンドユーザーになりすます権限を持っています", + "IAM_LOGIN_CLIENT": "ログインクライアントを管理する権限を持っています", "ORG_OWNER": "組織全体に対する権限を持ちます", "ORG_USER_MANAGER": "組織のユーザーを作成および管理する権限を持ちます", "ORG_OWNER_VIEWER": "組織全体を閲覧する権限を持ちます", + "ORG_SETTINGS_MANAGER": "組織の設定を管理する権限を持ちます", "ORG_USER_PERMISSION_EDITOR": "ユーザーグラントを管理する権限を持ちます", "ORG_PROJECT_PERMISSION_EDITOR": "プロジェクトグラントを管理する権限を持ちます", "ORG_PROJECT_CREATOR": "所有するプロジェクトと配下の設定を作成する権限を持ちます", "ORG_ADMIN_IMPERSONATOR": "組織の管理者およびエンドユーザーになりすます権限がある", "ORG_END_USER_IMPERSONATOR": "組織のエンドユーザーになりすます権限がある", + "ORG_USER_SELF_MANAGER": "自身を管理する権限がある", "PROJECT_OWNER": "特定のプロジェクト全体を管理する権限を持ちます", "PROJECT_OWNER_VIEWER": "特定のプロジェクト全体を閲覧する権限を持ちます", "PROJECT_OWNER_GLOBAL": "全てのプロジェクトを管理する権限を持ちます", @@ -787,7 +925,10 @@ "PHONESECTION": "電話番号", "PASSWORDSECTION": "初期パスワード", "ADDRESSANDPHONESECTION": "電話番号", - "INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。" + "INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。", + "SETUPAUTHENTICATIONLATER": "このユーザーの認証を後で設定します。", + "INVITATION": "認証設定とメール確認のための招待メールを送信してください。", + "INITIALPASSWORD": "ユーザーの初期パスワードを設定してください。" }, "CODEDIALOG": { "TITLE": "電話番号の検証", @@ -809,6 +950,9 @@ "EMAIL": "Eメール", "PHONE": "電話番号", "PHONE_HINT": "+ マークに続いて電話をかけたい国コードを入力するか、ドロップダウンから国を選択して電話番号を入力します。", + "PHONE_VERIFIED": "電話番号が確認されました", + "SEND_SMS": "認証SMSを送信", + "SEND_EMAIL": "メールを送信", "USERNAME": "ユーザー名", "CHANGEUSERNAME": "変更", "CHANGEUSERNAME_TITLE": "ユーザー名の変更", @@ -949,6 +1093,14 @@ "5": "停止", "6": "初期化待ち" }, + "STATEV2": { + "0": "不明", + "1": "アクティブ", + "2": "非アクティブ", + "3": "削除", + "4": "ロック", + "5": "初期化待ち" + }, "SEARCH": { "ADDITIONAL": "ログインネーム(現在の組織)", "ADDITIONAL-EXTERNAL": "ログインネーム(外部の組織)" @@ -1340,6 +1492,7 @@ "BRANDING": "ブランディング", "PRIVACYPOLICY": "プライバシーポリシー", "OIDC": "OIDCトークンのライフタイムと有効期限", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "シークレット設定", "SECURITY": "セキュリティ設定", "EVENTS": "イベント", @@ -1385,7 +1538,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1476,6 +1630,16 @@ "ACTIONS_DESCRIPTION": "Actions v2は、データの実行とターゲットを管理できます。フラグが有効になっている場合、新しい APIとその機能を使用できます。", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 セッション終了", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "フラグが有効になっている場合、id_token を `sid` クレームと共に id_token_hint として end_session エンドポイントに提供することで、ログイン UI から単一のセッションを終了できるようになります。 現在、同じユーザー エージェント (ブラウザ) からのすべてのセッションがログイン UI で終了することに注意してください。 セッション API を通じて管理されるセッションは、すでに単一のセッションの終了を許可しています。", + "DEBUGOIDCPARENTERROR": "デバッグ OIDC 親エラー", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "フラグが有効になっている場合、OIDC 親エラーはコンソールに記録されます。", + "DISABLEUSERTOKENEVENT": "ユーザートークンイベントを無効にする", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "バックチャネルログアウトを有効にする", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "バックチャネルログアウトは OpenID Connect バックチャネルログアウト 1.0 を実装し、OpenID プロバイダーでのセッション終了についてクライアントに通知するために使用できます。", + "PERMISSIONCHECKV2": "権限チェック V2", + "PERMISSIONCHECKV2_DESCRIPTION": "フラグが有効になっている場合、新しい API とその機能を使用できます。", + "WEBKEY": "ウェブキー", + "WEBKEY_DESCRIPTION": "フラグが有効になっている場合、新しい API とその機能を使用できます。", "STATES": { "INHERITED": "継承", "ENABLED": "有効", @@ -1486,7 +1650,12 @@ "ENABLED": "有効は継承されます", "DISABLED": "無効は継承されます" }, - "RESET": "すべて継承に設定" + "RESET": "すべて継承に設定", + "CONSOLEUSEV2USERAPI": "コンソールでユーザー作成のためにV2 APIを使用してください。", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "このフラグが有効化されると、コンソールはV2ユーザーAPIを使用して新しいユーザーを作成します。V2 APIでは、新しく作成されたユーザーは初期状態なしで開始します。", + "LOGINV2": "ログイン V2", + "LOGINV2_DESCRIPTION": "これを有効にすると、セキュリティ、パフォーマンス、およびカスタマイズ性が向上した、TypeScript ベースの新しいログイン UI が有効になります。", + "LOGINV2_BASEURI": "ベースURI" }, "DIALOG": { "RESET": { @@ -1534,6 +1703,10 @@ "MAXSIZEEXCEEDED": "最大サイズの524KBを超えました。", "NOSVGSUPPORTED": "SVGはサポートされていません!", "FONTINLOGINONLY": "フォントは現在、ログインインターフェイスにのみ表示されます。", + "BACKGROUNDCOLOR": "背景色", + "PRIMARYCOLOR": "プライマリカラー", + "WARNCOLOR": "警告色", + "FONTCOLOR": "フォントカラー", "VIEWS": { "PREVIEW": "プレビュー", "CURRENT": "現在の構成" @@ -1619,7 +1792,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "メール認証が完了しました", @@ -1669,7 +1844,8 @@ "PR": "パスワードのリセット", "DC": "ドメインクレーム", "PL": "パスワードレス", - "PC": "パスワードの変更" + "PC": "パスワードの変更", + "IU": "ユーザーの招待" }, "CHIPS": { "firstname": "名", @@ -2051,6 +2227,14 @@ "LDAP": { "TITLE": "LDAPプロバイダー", "DESCRIPTION": "LDAPプロバイダーのクレデンシャルを入力してください。" + }, + "APPLE": { + "TITLE": "Appleプロバイダー", + "DESCRIPTION": "Appleプロバイダーのクレデンシャルを入力してください。" + }, + "SAML": { + "TITLE": "SAMLプロバイダー", + "DESCRIPTION": "SAMLプロバイダーのクレデンシャルを入力してください。" } }, "DETAIL": { @@ -2170,6 +2354,23 @@ "JWTENDPOINT": "JWTエンドポイント", "JWTKEYSENDPOINT": "JWTキーエンドポイント" }, + "APPLE": { + "TEAMID": "チームID", + "KEYID": "キーID", + "PRIVATEKEY": "秘密鍵", + "UPDATEPRIVATEKEY": "秘密鍵の更新", + "UPLOADPRIVATEKEY": "秘密鍵のアップロード", + "KEYMAXSIZEEXCEEDED": "最大サイズの5kBを超えています" + }, + "SAML": { + "METADATAXML": "Metadata XML", + "METADATAURL": "Metadata URL", + "BINDING": "Binding", + "SIGNEDREQUEST": "署名付きリクエスト", + "NAMEIDFORMAT": "NameIDフォーマット", + "TRANSIENTMAPPINGATTRIBUTENAME": "カスタム属性名", + "TRANSIENTMAPPINGATTRIBUTENAME_DESC": "NameIDフォーマットが`transient`の場合にユーザーをマッピングするためのカスタム属性名 (例: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`)" + }, "TOAST": { "SAVED": "正常に保存されました。", "REACTIVATED": "IDPがアクティブになりました。", @@ -2182,7 +2383,9 @@ "REMOVED": "正常に削除されました。" }, "ISIDTOKENMAPPING": "IDトークンからのマッピング", - "ISIDTOKENMAPPING_DESC": "選択された場合、プロバイダ情報はIDトークンからマッピングされ、userinfoエンドポイントからではありません。" + "ISIDTOKENMAPPING_DESC": "選択された場合、プロバイダ情報はIDトークンからマッピングされ、userinfoエンドポイントからではありません。", + "USEPKCE": "PKCEを使用する", + "USEPKCE_DESC": "code_challenge パラメータと code_challenge_method パラメータが認証リクエストに含まれるかどうかを決定します。" }, "MFA": { "LIST": { @@ -2254,7 +2457,11 @@ "DELETE_TITLE": "SMTP設定を削除する", "DELETE_DESCRIPTION": "構成を削除しようとしています。送信者名を入力してこのアクションを確認します", "DELETED": "SMTP設定が削除されました", - "SENDER": "この SMTP 構成を削除するには、「{{ value }}」と入力します。" + "SENDER": "この SMTP 構成を削除するには、「{{ value }}」と入力します。", + "TEST_TITLE": "SMTP設定をテストする", + "TEST_DESCRIPTION": "テスト用のメールアドレスを指定してください", + "TEST_EMAIL": "メールアドレス", + "TEST_RESULT": "テスト結果" } }, "CREATE": { @@ -2561,7 +2768,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "マネージャーを追加する", diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json index 79fe95324a..b791234cd5 100644 --- a/console/src/assets/i18n/ko.json +++ b/console/src/assets/i18n/ko.json @@ -185,6 +185,32 @@ "DESCRIPTION": "유휴 갱신 토큰 수명은 갱신 토큰이 사용되지 않는 최대 기간을 의미합니다." } }, + "WEB_KEYS": { + "DESCRIPTION": "ZITADEL 인스턴스의 토큰을 안전하게 서명하고 검증하기 위해 OIDC 웹 키를 관리하세요.", + "TABLE": { + "TITLE": "활성 및 예정된 웹 키", + "DESCRIPTION": "현재 활성화된 웹 키와 앞으로 활성화될 웹 키입니다. 새로운 키를 활성화하면 기존 키는 비활성화됩니다.", + "NOTE": "참고: JWKs OIDC 엔드포인트는 캐시 가능한 응답을 반환합니다 (기본값: 5분). 키를 너무 빨리 활성화하면 캐시 및 클라이언트에서 아직 사용할 수 없을 수 있습니다.", + "ACTIVATE": "다음 웹 키 활성화", + "ACTIVE": "현재 활성화됨", + "NEXT": "대기 중인 다음 키", + "FUTURE": "향후 사용 예정", + "WARNING": "웹 키가 5분 미만입니다." + }, + "CREATE": { + "TITLE": "새 웹 키 생성", + "DESCRIPTION": "새 웹 키를 생성하면 목록에 추가됩니다. ZITADEL은 기본적으로 RSA2048 키와 SHA256 해시 알고리즘을 사용합니다.", + "KEY_TYPE": "키 유형", + "BITS": "비트", + "HASHER": "해시 알고리즘", + "CURVE": "곡선" + }, + "PREVIOUS_TABLE": { + "TITLE": "이전 웹 키", + "DESCRIPTION": "더 이상 활성 상태가 아닌 이전 웹 키 목록입니다.", + "DEACTIVATED_ON": "비활성화된 날짜" + } + }, "MESSAGE_TEXTS": { "TITLE": "메시지 텍스트", "DESCRIPTION": "알림 이메일 또는 SMS 메시지의 텍스트를 사용자 정의하세요. 언어를 비활성화하려면 인스턴스의 언어 설정에서 제한하세요.", @@ -502,6 +528,115 @@ "DOWNLOAD": "다운로드", "APPLY": "적용" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "작업", + "DESCRIPTION": "작업을 통해 API 요청, 이벤트 또는 특정 함수에 대한 응답으로 사용자 지정 코드를 실행할 수 있습니다. 이를 사용하여 Zitadel을 확장하고 워크플로를 자동화하며 다른 시스템과 통합합니다.", + "TYPES": { + "request": "요청", + "response": "응답", + "events": "이벤트", + "function": "함수" + }, + "DIALOG": { + "CREATE_TITLE": "작업 생성", + "UPDATE_TITLE": "작업 업데이트", + "TYPE": { + "DESCRIPTION": "이 작업을 실행할 시점을 선택하십시오.", + "REQUEST": { + "TITLE": "요청", + "DESCRIPTION": "Zitadel 내에서 발생하는 요청. 이는 로그인 요청 호출과 같은 것일 수 있습니다." + }, + "RESPONSE": { + "TITLE": "응답", + "DESCRIPTION": "Zitadel 내 요청으로부터의 응답. 사용자를 가져올 때 받는 응답을 생각해 보십시오." + }, + "EVENTS": { + "TITLE": "이벤트", + "DESCRIPTION": "Zitadel 내에서 발생하는 이벤트. 이는 사용자 계정 생성, 로그인 성공 등 모든 것이 될 수 있습니다." + }, + "FUNCTIONS": { + "TITLE": "함수", + "DESCRIPTION": "Zitadel 내에서 호출할 수 있는 함수입니다. 이는 이메일 전송부터 사용자 생성까지 모든 것이 될 수 있습니다." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "이 작업이 모든 요청, 특정 서비스(예: 사용자 관리) 또는 단일 요청(예: 사용자 생성)에 적용되는지 선택하십시오.", + "ALL": { + "TITLE": "모두", + "DESCRIPTION": "모든 요청에서 작업을 실행하려면 이것을 선택하십시오." + }, + "SELECT_SERVICE": { + "TITLE": "서비스 선택", + "DESCRIPTION": "작업에 대한 Zitadel 서비스를 선택하십시오." + }, + "SELECT_METHOD": { + "TITLE": "메서드 선택", + "DESCRIPTION": "특정 요청에서만 실행하려면 여기에서 선택하십시오.", + "NOTE": "메서드를 선택하지 않으면 선택한 서비스의 모든 요청에서 작업이 실행됩니다." + }, + "FUNCTIONNAME": { + "TITLE": "함수 이름", + "DESCRIPTION": "실행할 함수를 선택하십시오." + }, + "SELECT_GROUP": { + "TITLE": "그룹 설정", + "DESCRIPTION": "이벤트 그룹에서만 실행하려면 여기에서 그룹을 설정하십시오." + }, + "SELECT_EVENT": { + "TITLE": "이벤트 선택", + "DESCRIPTION": "특정 이벤트에서만 실행하려면 여기에서 지정하십시오." + } + }, + "TARGET": { + "DESCRIPTION": "타겟을 실행하거나 다른 타겟과 동일한 조건으로 실행하도록 선택할 수 있습니다.", + "TARGET": { + "DESCRIPTION": "이 작업에 대해 실행할 타겟" + }, + "CONDITIONS": { + "DESCRIPTION": "실행 조건" + } + } + }, + "TABLE": { + "CONDITION": "조건", + "TYPE": "유형", + "TARGET": "타겟", + "CREATIONDATE": "생성 날짜" + } + }, + "TARGET": { + "TITLE": "타겟", + "DESCRIPTION": "타겟은 작업에서 실행하려는 코드의 대상입니다. 여기에서 타겟을 생성하고 작업에 추가하십시오.", + "CREATE": { + "TITLE": "타겟 생성", + "DESCRIPTION": "Zitadel 외부에서 자체 타겟을 생성하십시오.", + "NAME": "이름", + "NAME_DESCRIPTION": "나중에 쉽게 식별할 수 있도록 타겟에 명확하고 설명적인 이름을 지정하십시오.", + "TYPE": "유형", + "TYPES": { + "restWebhook": "REST 웹훅", + "restCall": "REST 호출", + "restAsync": "REST 비동기" + }, + "ENDPOINT": "엔드포인트", + "ENDPOINT_DESCRIPTION": "코드가 호스팅되는 엔드포인트를 입력하십시오. 우리에게 액세스할 수 있는지 확인하십시오!", + "TIMEOUT": "시간 초과", + "TIMEOUT_DESCRIPTION": "타겟이 응답해야 하는 최대 시간을 설정하십시오. 시간이 더 오래 걸리면 요청을 중지합니다.", + "INTERRUPT_ON_ERROR": "오류 시 중단", + "INTERRUPT_ON_ERROR_DESCRIPTION": "타겟이 오류를 반환하면 모든 실행을 중지하십시오.", + "INTERRUPT_ON_ERROR_WARNING": "주의: “오류 시 중단” 기능은 실패 시 작업을 중단하며, 잠금 위험이 있습니다. 로그인/생성 차단을 방지하려면 비활성화된 상태로 테스트하세요.", + "AWAIT_RESPONSE": "응답 대기", + "AWAIT_RESPONSE_DESCRIPTION": "다른 작업을 수행하기 전에 응답을 기다립니다. 단일 작업에 여러 타겟을 사용하려는 경우 유용합니다." + }, + "TABLE": { + "NAME": "이름", + "ENDPOINT": "엔드포인트", + "CREATIONDATE": "생성 날짜", + "REORDER": "재정렬" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "인스턴스와 모든 조직에 대한 제어 권한이 있습니다", "IAM_OWNER_VIEWER": "인스턴스와 모든 조직을 검토할 수 있는 권한이 있습니다", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "사용자를 생성하고 관리할 수 있는 권한이 있습니다", "IAM_ADMIN_IMPERSONATOR": "모든 조직의 관리자와 최종 사용자를 대리할 수 있는 권한이 있습니다", "IAM_END_USER_IMPERSONATOR": "모든 조직의 최종 사용자를 대리할 수 있는 권한이 있습니다", + "IAM_LOGIN_CLIENT": "로그인 클라이언트를 관리할 수 있는 권한이 있습니다", "ORG_OWNER": "조직에 대한 전체 권한이 있습니다", "ORG_USER_MANAGER": "조직의 사용자를 생성하고 관리할 수 있는 권한이 있습니다", "ORG_OWNER_VIEWER": "조직 전체를 검토할 수 있는 권한이 있습니다", + "ORG_SETTINGS_MANAGER": "조직 설정을 관리할 수 있는 권한이 있습니다", "ORG_USER_PERMISSION_EDITOR": "사용자 권한을 관리할 수 있는 권한이 있습니다", "ORG_PROJECT_PERMISSION_EDITOR": "프로젝트 권한을 관리할 수 있는 권한이 있습니다", "ORG_PROJECT_CREATOR": "자신의 프로젝트와 하위 설정을 생성할 수 있는 권한이 있습니다", "ORG_ADMIN_IMPERSONATOR": "조직의 관리자 및 최종 사용자를 대리할 수 있는 권한이 있습니다", "ORG_END_USER_IMPERSONATOR": "조직의 최종 사용자를 대리할 수 있는 권한이 있습니다", + "ORG_USER_SELF_MANAGER": "자신의 사용자 계정을 관리할 수 있는 권한이 있습니다", "PROJECT_OWNER": "프로젝트에 대한 전체 권한이 있습니다", "PROJECT_OWNER_VIEWER": "프로젝트 전체를 검토할 수 있는 권한이 있습니다", "PROJECT_OWNER_GLOBAL": "프로젝트에 대한 전체 권한이 있습니다", @@ -787,7 +925,10 @@ "PHONESECTION": "전화번호", "PASSWORDSECTION": "초기 비밀번호", "ADDRESSANDPHONESECTION": "전화번호", - "INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다." + "INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다.", + "SETUPAUTHENTICATIONLATER": "이 사용자의 인증을 나중에 설정하세요.", + "INVITATION": "인증 설정 및 이메일 확인을 위한 초대 이메일을 보내세요.", + "INITIALPASSWORD": "사용자에 대한 초기 비밀번호를 설정하세요." }, "CODEDIALOG": { "TITLE": "전화번호 확인", @@ -809,6 +950,9 @@ "EMAIL": "이메일", "PHONE": "전화번호", "PHONE_HINT": "+ 기호 다음에 국가 코드를 입력하거나 드롭다운에서 국가를 선택한 후 전화번호를 입력하세요.", + "PHONE_VERIFIED": "전화번호 확인됨", + "SEND_SMS": "인증 SMS 보내기", + "SEND_EMAIL": "이메일 보내기", "USERNAME": "사용자 이름", "CHANGEUSERNAME": "수정", "CHANGEUSERNAME_TITLE": "사용자 이름 변경", @@ -949,6 +1093,14 @@ "5": "일시 중단됨", "6": "초기" }, + "STATEV2": { + "0": "알 수 없음", + "1": "활성", + "2": "비활성", + "3": "삭제됨", + "4": "잠김", + "5": "초기" + }, "SEARCH": { "ADDITIONAL": "로그인 이름 (현재 조직)", "ADDITIONAL-EXTERNAL": "로그인 이름 (외부 조직)" @@ -1340,6 +1492,7 @@ "BRANDING": "브랜딩", "PRIVACYPOLICY": "외부 링크", "OIDC": "OIDC 토큰 수명 및 만료", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "시크릿 생성기", "SECURITY": "보안 설정", "EVENTS": "이벤트", @@ -1385,7 +1538,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1476,6 +1630,16 @@ "ACTIONS_DESCRIPTION": "액션 v2는 데이터 실행 및 대상을 관리할 수 있습니다. 플래그가 활성화되면 새 API 및 기능을 사용할 수 있습니다.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC 단일 V1 세션 종료", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "플래그가 활성화되면, `sid` 클레임이 있는 id_token을 사용하여 end_session 엔드포인트에서 로그인 UI의 단일 세션을 종료할 수 있습니다. 현재 동일한 사용자 에이전트(브라우저)에서 모든 세션이 로그인 UI에서 종료됩니다. Session API를 통해 관리된 세션은 이미 단일 세션 종료를 허용합니다.", + "DEBUGOIDCPARENTERROR": "디버그 OIDC 부모 오류", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "플래그가 활성화되면 OIDC 부모 오류가 콘솔에 기록됩니다.", + "DISABLEUSERTOKENEVENT": "사용자 토큰 이벤트 비활성화", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "백채널 로그아웃 활성화", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "백채널 로그아웃은 OpenID Connect 백채널 로그아웃 1.0을 구현하며, OpenID 제공자에서 세션 종료에 대해 클라이언트에게 알리는 데 사용할 수 있습니다.", + "PERMISSIONCHECKV2": "권한 확인 V2", + "PERMISSIONCHECKV2_DESCRIPTION": "플래그가 활성화되면 새로운 API와 그 기능을 사용할 수 있습니다.", + "WEBKEY": "웹 키", + "WEBKEY_DESCRIPTION": "플래그가 활성화되면 새로운 API와 그 기능을 사용할 수 있습니다.", "STATES": { "INHERITED": "상속", "ENABLED": "활성화됨", @@ -1486,7 +1650,12 @@ "ENABLED": "\"활성화됨\"은 상속되었습니다.", "DISABLED": "\"비활성화됨\"은 상속되었습니다." }, - "RESET": "모두 상속으로 설정" + "RESET": "모두 상속으로 설정", + "CONSOLEUSEV2USERAPI": "콘솔에서 사용자 생성을 위해 V2 API를 사용하세요", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "이 플래그가 활성화되면 콘솔은 V2 사용자 API를 사용하여 새 사용자를 생성합니다. V2 API를 사용하면 새로 생성된 사용자는 초기 상태 없이 시작합니다.", + "LOGINV2": "로그인 V2", + "LOGINV2_DESCRIPTION": "이 옵션을 활성화하면 보안, 성능 및 사용자 정의 기능이 향상된 새로운 TypeScript 기반 로그인 UI가 활성화됩니다.", + "LOGINV2_BASEURI": "기본 URI" }, "DIALOG": { "RESET": { @@ -1623,7 +1792,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "이메일 인증 완료", @@ -2212,7 +2383,9 @@ "REMOVED": "성공적으로 제거되었습니다." }, "ISIDTOKENMAPPING": "ID 토큰에서 매핑", - "ISIDTOKENMAPPING_DESC": "선택 시, 사용자 정보 엔드포인트가 아닌 ID 토큰에서 제공자 정보를 매핑합니다." + "ISIDTOKENMAPPING_DESC": "선택 시, 사용자 정보 엔드포인트가 아닌 ID 토큰에서 제공자 정보를 매핑합니다.", + "USEPKCE": "PKCE 사용", + "USEPKCE_DESC": "code_challenge 및 code_challenge_method 매개변수가 인증 요청에 포함되는지 여부를 결정합니다" }, "MFA": { "LIST": { @@ -2591,7 +2764,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "매니저 추가", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 78d231ade7..22e0e6d3d7 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Животниот век на неактивниот токен за освежување е максималното време кое токен за освежување може да не се користи." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управувајте со вашите OIDC веб-клучеви за безбедно потпишување и валидација на токени за вашата ZITADEL инстанца.", + "TABLE": { + "TITLE": "Активни и Идни Веб-Клучеви", + "DESCRIPTION": "Вашите активни и претстојни веб-клучеви. Активирањето на нов клуч ќе го деактивира тековниот.", + "NOTE": "Забелешка: JWKs OIDC крајната точка враќа одговор што може да се кешира (стандардно 5 минути). Избегнувајте активирање на клучот пребрзо, бидејќи можеби сè уште не е достапен во кешот и кај клиентите.", + "ACTIVATE": "Активирај го следниот веб-клуч", + "ACTIVE": "Моментално активен", + "NEXT": "Следен во редот", + "FUTURE": "Иднина", + "WARNING": "Веб-клучот е помалку од 5 минути стар" + }, + "CREATE": { + "TITLE": "Креирај нов веб-клуч", + "DESCRIPTION": "Креирањето нов веб-клуч го додава на вашата листа. ZITADEL стандардно користи RSA2048 клучеви со SHA256 алгоритам за хаширање.", + "KEY_TYPE": "Тип на клуч", + "BITS": "Битови", + "HASHER": "Алгоритам за хаширање", + "CURVE": "Крива" + }, + "PREVIOUS_TABLE": { + "TITLE": "Претходни веб-клучеви", + "DESCRIPTION": "Ова се вашите претходни веб-клучеви кои повеќе не се активни.", + "DEACTIVATED_ON": "Деактивиран на" + } + }, "MESSAGE_TEXTS": { "TITLE": "Текстови на пораки", "DESCRIPTION": "Прилагодете ги текстовите на вашите е-маил или SMS пораки за известување. Ако сакате да оневозможите некои јазици, ограничете ги во поставките за јазик на вашите инстанци.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Преземи", "APPLY": "Пријавете се" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Акции", + "DESCRIPTION": "Акциите ви овозможуваат да извршувате прилагоден код како одговор на API барања, настани или специфични функции. Користете ги за да го проширите Zitadel, да ги автоматизирате работните процеси и да се интегрирате со други системи.", + "TYPES": { + "request": "Барање", + "response": "Одговор", + "events": "Настани", + "function": "Функција" + }, + "DIALOG": { + "CREATE_TITLE": "Креирај акција", + "UPDATE_TITLE": "Ажурирај акција", + "TYPE": { + "DESCRIPTION": "Изберете кога сакате да се изврши оваа акција", + "REQUEST": { + "TITLE": "Барање", + "DESCRIPTION": "Барања што се случуваат во Zitadel. Ова може да биде нешто како повик за барање за најава." + }, + "RESPONSE": { + "TITLE": "Одговор", + "DESCRIPTION": "Одговор од барање во Zitadel. Размислете за одговорот што го добивате при преземање на корисник." + }, + "EVENTS": { + "TITLE": "Настани", + "DESCRIPTION": "Настани што се случуваат во Zitadel. Ова може да биде нешто како корисник што креира сметка, успешна најава итн." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции што можете да ги повикате во Zitadel. Ова може да биде сè, од испраќање е-пошта до креирање корисник." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Изберете дали оваа акција се однесува на сите барања, одредена услуга (на пр. управување со корисници) или едно барање (на пр. креирај корисник).", + "ALL": { + "TITLE": "Сите", + "DESCRIPTION": "Изберете го ова ако сакате да ја извршите вашата акција на секое барање" + }, + "SELECT_SERVICE": { + "TITLE": "Изберете услуга", + "DESCRIPTION": "Изберете Zitadel услуга за вашата акција." + }, + "SELECT_METHOD": { + "TITLE": "Изберете метод", + "DESCRIPTION": "Ако сакате да извршите само на одредено барање, изберете го тука", + "NOTE": "Ако не изберете метод, вашата акција ќе се изврши на секое барање во вашата избрана услуга." + }, + "FUNCTIONNAME": { + "TITLE": "Име на функција", + "DESCRIPTION": "Изберете ја функцијата што сакате да ја извршите" + }, + "SELECT_GROUP": { + "TITLE": "Постави група", + "DESCRIPTION": "Ако сакате да извршите само на група настани, поставете ја групата тука" + }, + "SELECT_EVENT": { + "TITLE": "Изберете настан", + "DESCRIPTION": "Ако сакате да извршите само на одреден настан, наведете го тука" + } + }, + "TARGET": { + "DESCRIPTION": "Можете да изберете да извршите цел или да ја извршите под истите услови како и другите цели.", + "TARGET": { + "DESCRIPTION": "Целта што сакате да ја извршите за оваа акција" + }, + "CONDITIONS": { + "DESCRIPTION": "Услови за извршување" + } + } + }, + "TABLE": { + "CONDITION": "Услов", + "TYPE": "Тип", + "TARGET": "Цел", + "CREATIONDATE": "Датум на создавање" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Целта е дестинација на кодот што сакате да го извршите од акција. Креирајте цел овде и додајте ја на вашите акции.", + "CREATE": { + "TITLE": "Креирајте ја вашата цел", + "DESCRIPTION": "Креирајте ја вашата сопствена цел надвор од Zitadel", + "NAME": "Име", + "NAME_DESCRIPTION": "Дајте ѝ на вашата цел јасно, опис на име за да биде лесно да се идентификува подоцна", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Повик", + "restAsync": "REST Асинхроно" + }, + "ENDPOINT": "Крајна точка", + "ENDPOINT_DESCRIPTION": "Внесете ја крајната точка каде што е хостиран вашиот код. Осигурете се дека е достапна за нас!", + "TIMEOUT": "Време на истекување", + "TIMEOUT_DESCRIPTION": "Поставете го максималното време што вашата цел треба да одговори. Ако трае подолго, ќе го запреме барањето.", + "INTERRUPT_ON_ERROR": "Прекини при грешка", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Запрете ги сите извршувања кога целите ќе вратат грешка", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: „Прекини при грешка“ ги запира операциите при неуспех, со ризик од блокирање. Тестирајте со исклучена опција за да избегнете блокирање на најавата/креирањето.", + "AWAIT_RESPONSE": "Почекај одговор", + "AWAIT_RESPONSE_DESCRIPTION": "Ќе почекаме одговор пред да направиме нешто друго. Корисно ако планирате да користите повеќе цели за една акција" + }, + "TABLE": { + "NAME": "Име", + "ENDPOINT": "Крајна точка", + "CREATIONDATE": "Датум на создавање", + "REORDER": "Повторно нарачајте" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Има контрола врз целата инстанца, вклучувајќи ги сите организации", "IAM_OWNER_VIEWER": "Има дозвола за преглед на целата инстанца, вклучувајќи ги сите организации", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Има дозвола за креирање и менаџирање на корисници", "IAM_ADMIN_IMPERSONATOR": "Има дозвола да се претставува како администратор и крајни корисници од сите организации", "IAM_END_USER_IMPERSONATOR": "Има дозвола да ги имитира крајните корисници од сите организации", + "IAM_LOGIN_CLIENT": "Има дозвола за менаџирање на клиенти за најава", "ORG_OWNER": "Има дозвола врз целата организација", "ORG_USER_MANAGER": "Има дозвола за креирање и менаџирање на корисници во организацијата", "ORG_OWNER_VIEWER": "Има дозвола за преглед на целата организација", + "ORG_SETTINGS_MANAGER": "Има дозвола за менаџирање на подесувањата на организацијата", "ORG_USER_PERMISSION_EDITOR": "Има дозвола за менаџирање на овластувања на корисници", "ORG_PROJECT_PERMISSION_EDITOR": "Има дозвола за менаџирање на овластувања на проекти", "ORG_PROJECT_CREATOR": "Има дозвола за креирање на сопствени проекти и нивни подесувања", "ORG_ADMIN_IMPERSONATOR": "Има дозвола да имитира администратор и крајни корисници од организацијата", "ORG_END_USER_IMPERSONATOR": "Има дозвола да ги имитира крајните корисници од организацијата", + "ORG_USER_SELF_MANAGER": "Има дозвола за менаџирање на своите корисници", "PROJECT_OWNER": "Има дозвола врз целиот проект", "PROJECT_OWNER_VIEWER": "Има дозвола за преглед на целиот проект", "PROJECT_OWNER_GLOBAL": "Има дозвола врз целиот проект", @@ -787,7 +925,10 @@ "PHONESECTION": "Телефонски броеви", "PASSWORDSECTION": "Почетна лозинка", "ADDRESSANDPHONESECTION": "Телефонски број", - "INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците." + "INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците.", + "SETUPAUTHENTICATIONLATER": "Подесете автентикација подоцна за овој корисник.", + "INVITATION": "Испратете покана по е-пошта за поставување на автентикација и потврда на е-поштата.", + "INITIALPASSWORD": "Поставете почетна лозинка за корисникот." }, "CODEDIALOG": { "TITLE": "Верификација на телефонски број", @@ -809,6 +950,9 @@ "EMAIL": "Е-пошта", "PHONE": "Телефонски број", "PHONE_HINT": "Користете + и потоа дополнителниот број на земјата, или изберете ја земјата од листата и на крај внесете го телефонскиот број", + "PHONE_VERIFIED": "Телефонскиот број е потврден", + "SEND_SMS": "Испрати СМС за верификација", + "SEND_EMAIL": "Испрати е-пошта", "USERNAME": "Корисничко име", "CHANGEUSERNAME": "промени", "CHANGEUSERNAME_TITLE": "Промени корисничко име", @@ -949,6 +1093,14 @@ "5": "Суспендирано", "6": "Иницијално" }, + "STATEV2": { + "0": "Непознато", + "1": "Активно", + "2": "Неактивно", + "3": "Избришано", + "4": "Заклучено", + "5": "Иницијално" + }, "SEARCH": { "ADDITIONAL": "Корисничко име (тековна организација)", "ADDITIONAL-EXTERNAL": "Корисничко име (надворешна организација)" @@ -1341,6 +1493,7 @@ "BRANDING": "Брендирање", "PRIVACYPOLICY": "Политика за приватност", "OIDC": "OIDC времетраење и истекување на токени", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Изглед на тајни", "SECURITY": "Подесувања за безбедност", "EVENTS": "Настани", @@ -1386,7 +1539,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1477,6 +1631,16 @@ "ACTIONS_DESCRIPTION": "Акциите v2 овозможуваат управување со извршување на податоци и цели. Ако знамето е овозможено, ќе можете да го користите новиот API и неговите функции.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Завршување на сесија", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Ако ознаката е активирана, ќе можете да ја завршите единечна сесија од корисничкиот интерфејс за најава, со обезбедување id_token со `sid` побарување како id_token_hint на крајната точка на end_session. Имајте предвид дека во моментов сите сесии од истиот кориснички агент (прелистувач) се завршуваат во корисничкиот интерфејс за најава. Сесиите управувани преку API на сесија веќе дозволуваат завршување на единечни сесии.", + "DEBUGOIDCPARENTERROR": "Дебагирање на OIDC родителска грешка", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Ако знамето е овозможено, грешката на OIDC родителот ќе биде регистрирана во конзолата.", + "DISABLEUSERTOKENEVENT": "Оневозможи настан за кориснички токен", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Овозможи Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout имплементира OpenID Connect Back-Channel Logout 1.0 и може да се користи за известување на клиентите за завршување на сесијата кај OpenID провајдерот.", + "PERMISSIONCHECKV2": "Проверка на дозволи V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Ако знамето е овозможено, ќе можете да ја користите новата API и нејзините функции.", + "WEBKEY": "Веб клуч", + "WEBKEY_DESCRIPTION": "Ако знамето е овозможено, ќе можете да ја користите новата API и нејзините функции.", "STATES": { "INHERITED": "Наследи", "ENABLED": "Овозможено", @@ -1487,7 +1651,12 @@ "ENABLED": "„Овозможено“ е наследено", "DISABLED": "„Оневозможено“ е наследено" }, - "RESET": "Поставете ги сите да наследат" + "RESET": "Поставете ги сите да наследат", + "CONSOLEUSEV2USERAPI": "Користете V2 API во конзолата за креирање на корисници", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Кога ова знаме е овозможено, конзолата го користи V2 User API за креирање на нови корисници. Со V2 API, новосоздадените корисници започнуваат без почетна состојба.", + "LOGINV2": "Најава V2", + "LOGINV2_DESCRIPTION": "Овозможувањето на ова ја активира новата TypeScript-базирана најава со подобрена безбедност, перформанси и прилагодливост.", + "LOGINV2_BASEURI": "Основен URI" }, "DIALOG": { "RESET": { @@ -1624,7 +1793,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Е-поштата е верифицирана", @@ -2188,7 +2359,9 @@ "REMOVED": "Успешно отстрането." }, "ISIDTOKENMAPPING": "Совпаѓање од ID токен", - "ISIDTOKENMAPPING_DESC": "Ако е избрано, информациите од провајдерот се мапираат од ID токенот, а не од userinfo крајната точка." + "ISIDTOKENMAPPING_DESC": "Ако е избрано, информациите од провајдерот се мапираат од ID токенот, а не од userinfo крајната точка.", + "USEPKCE": "Користете PKCE", + "USEPKCE_DESC": "Определува дали параметрите code_challenge и code_challenge_method се вклучени во барањето за авторизација" }, "MFA": { "LIST": { @@ -2567,7 +2740,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Додај Менаџер", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index cb1a66469a..112474e770 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -185,6 +185,32 @@ "DESCRIPTION": "De levensduur van het inactieve vernieuwingstoken is de maximale tijd dat een vernieuwingstoken ongebruikt kan zijn." } }, + "WEB_KEYS": { + "DESCRIPTION": "Beheer je OIDC Web Keys om tokens veilig te ondertekenen en te valideren voor je ZITADEL-instantie.", + "TABLE": { + "TITLE": "Actieve en Toekomstige Websleutels", + "DESCRIPTION": "Je actieve en aankomende websleutels. Het activeren van een nieuwe sleutel deactiveert de huidige.", + "NOTE": "Opmerking: Het JWKs OIDC-eindpunt geeft een cachebare respons terug (standaard 5 minuten). Vermijd het te vroeg activeren van een sleutel, omdat deze mogelijk nog niet beschikbaar is in caches en bij clients.", + "ACTIVATE": "Volgende Websleutel activeren", + "ACTIVE": "Momenteel actief", + "NEXT": "Volgende in de wachtrij", + "FUTURE": "Toekomstig", + "WARNING": "De websleutel is minder dan 5 minuten oud" + }, + "CREATE": { + "TITLE": "Nieuwe Websleutel aanmaken", + "DESCRIPTION": "Het aanmaken van een nieuwe websleutel voegt deze toe aan je lijst. ZITADEL gebruikt standaard RSA2048-sleutels met een SHA256-hasher.", + "KEY_TYPE": "Sleuteltype", + "BITS": "Bits", + "HASHER": "Hasher", + "CURVE": "Curve" + }, + "PREVIOUS_TABLE": { + "TITLE": "Vorige Websleutels", + "DESCRIPTION": "Dit zijn je vorige websleutels die niet langer actief zijn.", + "DEACTIVATED_ON": "Gedeactiveerd op" + } + }, "MESSAGE_TEXTS": { "TITLE": "Berichtteksten", "DESCRIPTION": "Pas de teksten van je notificatie-e-mail of SMS-berichten aan. Als je sommige talen wilt uitschakelen, beperk ze dan in de taalinstellingen van je instanties.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Download", "APPLY": "Toepassen" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Acties", + "DESCRIPTION": "Met acties kunt u aangepaste code uitvoeren als reactie op API-verzoeken, gebeurtenissen of specifieke functies. Gebruik ze om Zitadel uit te breiden, workflows te automatiseren en te integreren met andere systemen.", + "TYPES": { + "request": "Verzoek", + "response": "Reactie", + "events": "Gebeurtenissen", + "function": "Functie" + }, + "DIALOG": { + "CREATE_TITLE": "Een actie maken", + "UPDATE_TITLE": "Een actie bijwerken", + "TYPE": { + "DESCRIPTION": "Selecteer wanneer u deze actie wilt uitvoeren", + "REQUEST": { + "TITLE": "Verzoek", + "DESCRIPTION": "Verzoeken die binnen Zitadel plaatsvinden. Dit kan zoiets zijn als een inlogverzoek-oproep." + }, + "RESPONSE": { + "TITLE": "Reactie", + "DESCRIPTION": "Een reactie op een verzoek binnen Zitadel. Denk aan de reactie die u terugkrijgt van het ophalen van een gebruiker." + }, + "EVENTS": { + "TITLE": "Gebeurtenissen", + "DESCRIPTION": "Gebeurtenissen die binnen Zitadel plaatsvinden. Dit kan van alles zijn, zoals een gebruiker die een account aanmaakt, een succesvolle login enz." + }, + "FUNCTIONS": { + "TITLE": "Functies", + "DESCRIPTION": "Functies die u binnen Zitadel kunt aanroepen. Dit kan van alles zijn, van het verzenden van een e-mail tot het maken van een gebruiker." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Kies of deze actie van toepassing is op alle verzoeken, een specifieke service (bijv. gebruikersbeheer) of een enkel verzoek (bijv. gebruiker maken).", + "ALL": { + "TITLE": "Alle", + "DESCRIPTION": "Selecteer dit als u uw actie bij elk verzoek wilt uitvoeren" + }, + "SELECT_SERVICE": { + "TITLE": "Service selecteren", + "DESCRIPTION": "Kies een Zitadel-service voor uw actie." + }, + "SELECT_METHOD": { + "TITLE": "Methode selecteren", + "DESCRIPTION": "Als u alleen bij een specifiek verzoek wilt uitvoeren, selecteert u dit hier", + "NOTE": "Als u geen methode selecteert, wordt uw actie bij elk verzoek in uw geselecteerde service uitgevoerd." + }, + "FUNCTIONNAME": { + "TITLE": "Functienaam", + "DESCRIPTION": "Kies de functie die u wilt uitvoeren" + }, + "SELECT_GROUP": { + "TITLE": "Groep instellen", + "DESCRIPTION": "Als u alleen bij een groep gebeurtenissen wilt uitvoeren, stelt u de groep hier in" + }, + "SELECT_EVENT": { + "TITLE": "Gebeurtenis selecteren", + "DESCRIPTION": "Als u alleen bij een specifieke gebeurtenis wilt uitvoeren, specificeert u deze hier" + } + }, + "TARGET": { + "DESCRIPTION": "U kunt ervoor kiezen om een doel uit te voeren of om het onder dezelfde voorwaarden als andere doelen uit te voeren.", + "TARGET": { + "DESCRIPTION": "Het doel dat u voor deze actie wilt uitvoeren" + }, + "CONDITIONS": { + "DESCRIPTION": "Uitvoeringsvoorwaarden" + } + } + }, + "TABLE": { + "CONDITION": "Voorwaarde", + "TYPE": "Type", + "TARGET": "Doel", + "CREATIONDATE": "Aanmaakdatum" + } + }, + "TARGET": { + "TITLE": "Doelen", + "DESCRIPTION": "Een doel is de bestemming van de code die u vanuit een actie wilt uitvoeren. Maak hier een doel en voeg het toe aan uw acties.", + "CREATE": { + "TITLE": "Uw doel maken", + "DESCRIPTION": "Maak uw eigen doel buiten Zitadel", + "NAME": "Naam", + "NAME_DESCRIPTION": "Geef uw doel een duidelijke, beschrijvende naam om het later gemakkelijk te kunnen identificeren", + "TYPE": "Type", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Aanroep", + "restAsync": "REST Asynchroon" + }, + "ENDPOINT": "Eindpunt", + "ENDPOINT_DESCRIPTION": "Voer het eindpunt in waar uw code wordt gehost. Zorg ervoor dat het voor ons toegankelijk is!", + "TIMEOUT": "Time-out", + "TIMEOUT_DESCRIPTION": "Stel de maximale tijd in die uw doel heeft om te reageren. Als het langer duurt, stoppen we het verzoek.", + "INTERRUPT_ON_ERROR": "Onderbreken bij fout", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stop alle uitvoeringen als de doelen een fout retourneren", + "INTERRUPT_ON_ERROR_WARNING": "Let op: “Onderbreken bij fout” stopt operaties bij een mislukking, met kans op blokkering. Test met deze optie uitgeschakeld om inloggen/aanmaken niet te blokkeren.", + "AWAIT_RESPONSE": "Wachten op reactie", + "AWAIT_RESPONSE_DESCRIPTION": "We wachten op een reactie voordat we iets anders doen. Handig als u van plan bent om meerdere doelen voor één actie te gebruiken" + }, + "TABLE": { + "NAME": "Naam", + "ENDPOINT": "Eindpunt", + "CREATIONDATE": "Aanmaakdatum", + "REORDER": "Opnieuw ordenen" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Heeft controle over de hele instantie, inclusief alle organisaties", "IAM_OWNER_VIEWER": "Heeft toestemming om de hele instantie te bekijken, inclusief alle organisaties", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Heeft toestemming om gebruikers aan te maken en te beheren", "IAM_ADMIN_IMPERSONATOR": "Heeft toestemming om zich voor te doen als beheerder en eindgebruikers van alle organisaties", "IAM_END_USER_IMPERSONATOR": "Heeft toestemming om eindgebruikers van alle organisaties na te bootsen", + "IAM_LOGIN_CLIENT": "Heeft toestemming om aanmeldklanten te beheren", "ORG_OWNER": "Heeft toestemming over de hele organisatie", "ORG_USER_MANAGER": "Heeft toestemming om gebruikers van de organisatie aan te maken en te beheren", "ORG_OWNER_VIEWER": "Heeft toestemming om de hele organisatie te bekijken", + "ORG_SETTINGS_MANAGER": "Heeft toestemming om de instellingen van de organisatie te beheren", "ORG_USER_PERMISSION_EDITOR": "Heeft toestemming om gebruikerstoegang te beheren", "ORG_PROJECT_PERMISSION_EDITOR": "Heeft toestemming om projecttoegang te beheren", "ORG_PROJECT_CREATOR": "Heeft toestemming om zijn eigen projecten en onderliggende instellingen aan te maken", "ORG_ADMIN_IMPERSONATOR": "Heeft toestemming om de beheerder en eindgebruikers van de organisatie na te bootsen", "ORG_END_USER_IMPERSONATOR": "Heeft toestemming om eindgebruikers van de organisatie na te bootsen", + "ORG_USER_SELF_MANAGER": "Heeft toestemming om zijn eigen gebruiker te beheren", "PROJECT_OWNER": "Heeft toestemming over het hele project", "PROJECT_OWNER_VIEWER": "Heeft toestemming om het hele project te bekijken", "PROJECT_OWNER_GLOBAL": "Heeft toestemming over het hele project", @@ -787,7 +925,10 @@ "PHONESECTION": "Telefoonnummers", "PASSWORDSECTION": "Initieel wachtwoord", "ADDRESSANDPHONESECTION": "Telefoonnummer", - "INITMAILDESCRIPTION": "Als beide opties geselecteerd zijn, wordt er geen e-mail voor initialisatie verzonden. Als slechts een van de opties is geselecteerd, wordt een e-mail gestuurd om de gegevens te verstrekken / te verifiëren." + "INITMAILDESCRIPTION": "Als beide opties geselecteerd zijn, wordt er geen e-mail voor initialisatie verzonden. Als slechts een van de opties is geselecteerd, wordt een e-mail gestuurd om de gegevens te verstrekken / te verifiëren.", + "SETUPAUTHENTICATIONLATER": "Authenticatie later instellen voor deze gebruiker.", + "INVITATION": "Stuur een uitnodigingsmail voor het instellen van authenticatie en e-mailverificatie.", + "INITIALPASSWORD": "Stel een initieel wachtwoord in voor de gebruiker." }, "CODEDIALOG": { "TITLE": "Verifieer telefoonnummer", @@ -809,6 +950,9 @@ "EMAIL": "E-mail", "PHONE": "Telefoonnummer", "PHONE_HINT": "Gebruik het + symbool gevolgd door de landcode, of selecteer het land uit de dropdown en voer ten slotte het telefoonnummer in", + "PHONE_VERIFIED": "Telefoonnummer geverifieerd", + "SEND_SMS": "Verificatie-SMS verzenden", + "SEND_EMAIL": "E-mail verzenden", "USERNAME": "Gebruikersnaam", "CHANGEUSERNAME": "wijzigen", "CHANGEUSERNAME_TITLE": "Gebruikersnaam wijzigen", @@ -949,6 +1093,14 @@ "5": "Opgeschort", "6": "Initieel" }, + "STATEV2": { + "0": "Onbekend", + "1": "Actief", + "2": "Inactief", + "3": "Verwijderd", + "4": "Vergrendeld", + "5": "Initieel" + }, "SEARCH": { "ADDITIONAL": "Loginnaam (huidige organisatie)", "ADDITIONAL-EXTERNAL": "Loginnaam (externe organisatie)" @@ -1340,6 +1492,7 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Privacybeleid", "OIDC": "OIDC Token levensduur en vervaldatum", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Secret Generator", "SECURITY": "Beveiligingsinstellingen", "EVENTS": "Evenementen", @@ -1383,7 +1536,10 @@ "ru": "Русский", "nl": "Nederlands", "sv": "Svenska", - "hu": "Magyar" + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1474,6 +1630,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 maken het mogelijk om data-uitvoeringen en doelen te beheren. Als de vlag is ingeschakeld, kunt u de nieuwe API en zijn functies gebruiken.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Sessiebeëindiging", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Als het vlagje is ingeschakeld, kunt u een enkele sessie beëindigen via de login-gebruikersinterface door een id_token met een `sid`-claim als id_token_hint op het eindpunt end_session te verstrekken. Houd er rekening mee dat momenteel alle sessies van dezelfde gebruikersagent (browser) worden beëindigd in de login-gebruikersinterface. Sessies die worden beheerd via de Session API staan al toe om individuele sessies te beëindigen.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Ouderfout", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Als de vlag is ingeschakeld, wordt de OIDC-ouderfout in de console geregistreerd.", + "DISABLEUSERTOKENEVENT": "Gebruikerstokengebeurtenis uitschakelen", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Backchannel Logout inschakelen", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "De Back-Channel Logout implementeert OpenID Connect Back-Channel Logout 1.0 en kan worden gebruikt om clients te informeren over het beëindigen van de sessie bij de OpenID-provider.", + "PERMISSIONCHECKV2": "Permissiecontrole V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Als de vlag is ingeschakeld, kunt u de nieuwe API en de bijbehorende functies gebruiken.", + "WEBKEY": "Websleutel", + "WEBKEY_DESCRIPTION": "Als de vlag is ingeschakeld, kunt u de nieuwe API en de bijbehorende functies gebruiken.", "STATES": { "INHERITED": "Overgenomen", "ENABLED": "Ingeschakeld", @@ -1484,7 +1650,12 @@ "ENABLED": "\"Ingeschakeld\" wordt overgenomen", "DISABLED": "\"Uitgeschakeld\" wordt overgenomen" }, - "RESET": "Alles instellen op overgenomen" + "RESET": "Alles instellen op overgenomen", + "CONSOLEUSEV2USERAPI": "Gebruik de V2 API in de console voor het aanmaken van gebruikers", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Wanneer deze vlag is ingeschakeld, gebruikt de console de V2 User API om nieuwe gebruikers aan te maken. Met de V2 API beginnen nieuw aangemaakte gebruikers zonder een initiële status.", + "LOGINV2": "Inloggen V2", + "LOGINV2_DESCRIPTION": "Door dit in te schakelen wordt de nieuwe TypeScript-gebaseerde login-UI geactiveerd met verbeterde beveiliging, prestaties en aanpasbaarheid.", + "LOGINV2_BASEURI": "Basis-URI" }, "DIALOG": { "RESET": { @@ -1621,7 +1792,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "E-mail verificatie voltooid", @@ -2207,7 +2380,9 @@ "REMOVED": "Succesvol verwijderd." }, "ISIDTOKENMAPPING": "Kaart van de ID token", - "ISIDTOKENMAPPING_DESC": "Als geselecteerd, wordt provider informatie in kaart gebracht van de ID token, niet van de userinfo eindpunt." + "ISIDTOKENMAPPING_DESC": "Als geselecteerd, wordt provider informatie in kaart gebracht van de ID token, niet van de userinfo eindpunt.", + "USEPKCE": "Gebruik PKCE", + "USEPKCE_DESC": "Bepaalt of de parameters code_challenge en code_challenge_method zijn opgenomen in het verificatieverzoek" }, "MFA": { "LIST": { @@ -2586,7 +2761,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Voeg een Manager toe", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 934e569d9a..3244ccb4a6 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Czas życia bezczynnego tokena odświeżania to maksymalny czas, przez który token odświeżania może pozostać nieużywany." } }, + "WEB_KEYS": { + "DESCRIPTION": "Zarządzaj swoimi kluczami internetowymi OIDC, aby bezpiecznie podpisywać i weryfikować tokeny w swojej instancji ZITADEL.", + "TABLE": { + "TITLE": "Aktywne i Przyszłe Klucze Internetowe", + "DESCRIPTION": "Twoje aktywne i nadchodzące klucze internetowe. Aktywacja nowego klucza spowoduje dezaktywację obecnego.", + "NOTE": "Uwaga: Punkt końcowy JWKs OIDC zwraca odpowiedź możliwą do buforowania (domyślnie 5 minut). Unikaj zbyt wczesnej aktywacji klucza, ponieważ może on nie być jeszcze dostępny w pamięci podręcznej i dla klientów.", + "ACTIVATE": "Aktywuj następny klucz internetowy", + "ACTIVE": "Obecnie aktywny", + "NEXT": "Następny w kolejce", + "FUTURE": "Przyszłe", + "WARNING": "Klucz sieciowy ma mniej niż 5 minut" + }, + "CREATE": { + "TITLE": "Utwórz nowy klucz internetowy", + "DESCRIPTION": "Utworzenie nowego klucza internetowego doda go do Twojej listy. ZITADEL domyślnie używa kluczy RSA2048 z haszowaniem SHA256.", + "KEY_TYPE": "Typ klucza", + "BITS": "Bity", + "HASHER": "Haszowanie", + "CURVE": "Krzywa" + }, + "PREVIOUS_TABLE": { + "TITLE": "Poprzednie Klucze Internetowe", + "DESCRIPTION": "To są Twoje poprzednie klucze internetowe, które nie są już aktywne.", + "DEACTIVATED_ON": "Dezaktywowany dnia" + } + }, "MESSAGE_TEXTS": { "TITLE": "Teksty wiadomości", "DESCRIPTION": "Dostosuj teksty swoich e-maili lub wiadomości SMS z powiadomieniami. Jeśli chcesz wyłączyć niektóre języki, ogranicz je w ustawieniach językowych swoich instancji.", @@ -501,6 +527,115 @@ "DOWNLOAD": "Pobierz", "APPLY": "Stosować" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Akcje", + "DESCRIPTION": "Akcje umożliwiają uruchamianie niestandardowego kodu w odpowiedzi na żądania API, zdarzenia lub określone funkcje. Użyj ich, aby rozszerzyć Zitadel, zautomatyzować przepływy pracy i zintegrować się z innymi systemami.", + "TYPES": { + "request": "Żądanie", + "response": "Odpowiedź", + "events": "Zdarzenia", + "function": "Funkcja" + }, + "DIALOG": { + "CREATE_TITLE": "Utwórz akcję", + "UPDATE_TITLE": "Aktualizuj akcję", + "TYPE": { + "DESCRIPTION": "Wybierz, kiedy chcesz uruchomić tę akcję", + "REQUEST": { + "TITLE": "Żądanie", + "DESCRIPTION": "Żądania występujące w Zitadel. Może to być coś takiego jak wywołanie żądania logowania." + }, + "RESPONSE": { + "TITLE": "Odpowiedź", + "DESCRIPTION": "Odpowiedź na żądanie w Zitadel. Pomyśl o odpowiedzi, którą otrzymujesz po pobraniu użytkownika." + }, + "EVENTS": { + "TITLE": "Zdarzenia", + "DESCRIPTION": "Zdarzenia, które mają miejsce w Zitadel. Mogą to być dowolne zdarzenia, takie jak utworzenie konta użytkownika, udane logowanie itp." + }, + "FUNCTIONS": { + "TITLE": "Funkcje", + "DESCRIPTION": "Funkcje, które można wywołać w Zitadel. Mogą to być dowolne funkcje, od wysłania wiadomości e-mail po utworzenie użytkownika." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Wybierz, czy ta akcja ma zastosowanie do wszystkich żądań, określonej usługi (np. zarządzanie użytkownikami) czy pojedynczego żądania (np. utwórz użytkownika).", + "ALL": { + "TITLE": "Wszystkie", + "DESCRIPTION": "Wybierz tę opcję, jeśli chcesz uruchomić akcję dla każdego żądania" + }, + "SELECT_SERVICE": { + "TITLE": "Wybierz usługę", + "DESCRIPTION": "Wybierz usługę Zitadel dla swojej akcji." + }, + "SELECT_METHOD": { + "TITLE": "Wybierz metodę", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla określonego żądania, wybierz je tutaj", + "NOTE": "Jeśli nie wybierzesz metody, akcja zostanie uruchomiona dla każdego żądania w wybranej usłudze." + }, + "FUNCTIONNAME": { + "TITLE": "Nazwa funkcji", + "DESCRIPTION": "Wybierz funkcję, którą chcesz uruchomić" + }, + "SELECT_GROUP": { + "TITLE": "Ustaw grupę", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla grupy zdarzeń, ustaw grupę tutaj" + }, + "SELECT_EVENT": { + "TITLE": "Wybierz zdarzenie", + "DESCRIPTION": "Jeśli chcesz uruchomić tylko dla określonego zdarzenia, określ je tutaj" + } + }, + "TARGET": { + "DESCRIPTION": "Możesz wybrać uruchomienie celu lub uruchomienie go na tych samych warunkach co inne cele.", + "TARGET": { + "DESCRIPTION": "Cel, który chcesz uruchomić dla tej akcji" + }, + "CONDITIONS": { + "DESCRIPTION": "Warunki wykonania" + } + } + }, + "TABLE": { + "CONDITION": "Warunek", + "TYPE": "Typ", + "TARGET": "Cel", + "CREATIONDATE": "Data utworzenia" + } + }, + "TARGET": { + "TITLE": "Cele", + "DESCRIPTION": "Celem jest miejsce docelowe kodu, który chcesz uruchomić z akcji. Utwórz cel tutaj i dodaj go do swoich akcji.", + "CREATE": { + "TITLE": "Utwórz swój cel", + "DESCRIPTION": "Utwórz własny cel poza Zitadel", + "NAME": "Nazwa", + "NAME_DESCRIPTION": "Nadaj swojemu celowi jasną, opisową nazwę, aby ułatwić jego późniejszą identyfikację", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Wywołanie REST", + "restAsync": "REST Asynchroniczny" + }, + "ENDPOINT": "Punkt końcowy", + "ENDPOINT_DESCRIPTION": "Wprowadź punkt końcowy, w którym hostowany jest Twój kod. Upewnij się, że jest dla nas dostępny!", + "TIMEOUT": "Limit czasu", + "TIMEOUT_DESCRIPTION": "Ustaw maksymalny czas, w jakim cel musi odpowiedzieć. Jeśli zajmie to więcej czasu, zatrzymamy żądanie.", + "INTERRUPT_ON_ERROR": "Przerwij w przypadku błędu", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Zatrzymaj wszystkie wykonania, gdy cele zwrócą błąd", + "INTERRUPT_ON_ERROR_WARNING": "Uwaga: „Przerwij w przypadku błędu” zatrzymuje operacje w przypadku błędu, co grozi zablokowaniem. Przetestuj przy wyłączonej opcji, aby uniknąć blokowania logowania/tworzenia.", + "AWAIT_RESPONSE": "Oczekuj na odpowiedź", + "AWAIT_RESPONSE_DESCRIPTION": "Przed wykonaniem jakichkolwiek innych czynności poczekamy na odpowiedź. Przydatne, jeśli zamierzasz użyć wielu celów dla jednej akcji" + }, + "TABLE": { + "NAME": "Nazwa", + "ENDPOINT": "Punkt końcowy", + "CREATIONDATE": "Data utworzenia", + "REORDER": "Zmień kolejność" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Ma kontrolę nad całą instancją, włącznie z wszystkimi organizacjami", "IAM_OWNER_VIEWER": "Ma uprawnienie do przeglądania całej instancji, włącznie z wszystkimi organizacjami", @@ -508,14 +643,17 @@ "IAM_USER_MANAGER": "Ma uprawnienie do tworzenia i zarządzania użytkownikami", "IAM_ADMIN_IMPERSONATOR": "Ma uprawnienia do podszywania się pod administratora i użytkowników końcowych ze wszystkich organizacji", "IAM_END_USER_IMPERSONATOR": "Ma uprawnienia do podszywania się pod użytkowników końcowych ze wszystkich organizacji", + "IAM_LOGIN_CLIENT": "Ma uprawnienia do zarządzania klientami logowania", "ORG_OWNER": "Ma uprawnienie nad całą organizacją", "ORG_USER_MANAGER": "Ma uprawnienie do tworzenia i zarządzania użytkownikami organizacji", "ORG_OWNER_VIEWER": "Ma uprawnienie do przeglądania całej organizacji", + "ORG_SETTINGS_MANAGER": "Ma uprawnienie do zarządzania ustawieniami organizacji", "ORG_USER_PERMISSION_EDITOR": "Ma uprawnienie do zarządzania uprawnieniami użytkowników", "ORG_PROJECT_PERMISSION_EDITOR": "Ma uprawnienie do zarządzania uprawnieniami projektu", "ORG_PROJECT_CREATOR": "Ma uprawnienie do tworzenia własnych projektów i podstawowych ustawień", "ORG_ADMIN_IMPERSONATOR": "Ma uprawnienia do podszywania się pod administratora i użytkowników końcowych z organizacji", "ORG_END_USER_IMPERSONATOR": "Ma uprawnienia do podszywania się pod użytkowników końcowych z organizacji", + "ORG_USER_SELF_MANAGER": "Ma uprawnienie do zarządzania swoim własnym kontem użytkownika", "PROJECT_OWNER": "Ma uprawnienie nad całym projektem", "PROJECT_OWNER_VIEWER": "Ma uprawnienie do przeglądania całego projektu", "PROJECT_OWNER_GLOBAL": "Ma uprawnienia do całego projektu", @@ -786,7 +924,10 @@ "PHONESECTION": "Numery telefonów", "PASSWORDSECTION": "Hasło początkowe", "ADDRESSANDPHONESECTION": "Numer telefonu", - "INITMAILDESCRIPTION": "Jeśli zaznaczone są obie opcje, nie zostanie wysłany żaden e-mail inicjujący. Jeśli zaznaczona jest tylko jedna opcja, zostanie wysłany e-mail, aby udostępnić/zweryfikować dane." + "INITMAILDESCRIPTION": "Jeśli zaznaczone są obie opcje, nie zostanie wysłany żaden e-mail inicjujący. Jeśli zaznaczona jest tylko jedna opcja, zostanie wysłany e-mail, aby udostępnić/zweryfikować dane.", + "SETUPAUTHENTICATIONLATER": "Skonfiguruj uwierzytelnianie później dla tego użytkownika.", + "INVITATION": "Wyślij e-mail zaproszeniowy do konfiguracji uwierzytelniania i weryfikacji e-maila.", + "INITIALPASSWORD": "Ustaw początkowe hasło dla użytkownika." }, "CODEDIALOG": { "TITLE": "Weryfikuj numer telefonu", @@ -808,6 +949,9 @@ "EMAIL": "E-mail", "PHONE": "Numer telefonu", "PHONE_HINT": "Użyj symbolu +, a następnie kodu kraju, z którego dzwonisz, lub wybierz kraj z listy rozwijanej i wprowadź numer telefonu.", + "PHONE_VERIFIED": "Numer telefonu zweryfikowany", + "SEND_SMS": "Wyślij SMS weryfikacyjny", + "SEND_EMAIL": "Wyślij E-mail", "USERNAME": "Nazwa użytkownika", "CHANGEUSERNAME": "modyfikuj", "CHANGEUSERNAME_TITLE": "Zmień nazwę użytkownika", @@ -948,6 +1092,14 @@ "5": "Zawieszony", "6": "Początkowy" }, + "STATEV2": { + "0": "Nieznany", + "1": "Aktywny", + "2": "Nieaktywny", + "3": "Usunięty", + "4": "Zablokowany", + "5": "Początkowy" + }, "SEARCH": { "ADDITIONAL": "Nazwa użytkownika (obecna organizacja)", "ADDITIONAL-EXTERNAL": "Nazwa użytkownika (organizacja zewnętrzna)" @@ -1339,6 +1491,7 @@ "BRANDING": "Marka", "PRIVACYPOLICY": "Polityka prywatności", "OIDC": "Czas trwania tokenów OIDC i wygaśnięcie", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Wygląd sekretów", "SECURITY": "Ustawienia bezpieczeństwa", "EVENTS": "Zdarzenia", @@ -1384,7 +1537,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1475,6 +1629,16 @@ "ACTIONS_DESCRIPTION": "Akcje v2 umożliwiają zarządzanie wykonaniami danych i celami. Jeżeli flaga jest włączona, będziesz mógł korzystać z nowego interfejsu API i jego funkcji.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Zakończenie sesji", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł zakończyć pojedynczą sesję z interfejsu użytkownika logowania, podając id_token z roszczeniem `sid` jako id_token_hint w punkcie końcowym end_session. Należy pamiętać, że obecnie wszystkie sesje z tego samego agenta użytkownika (przeglądarki) są kończone w interfejsie użytkownika logowania. Sesje zarządzane za pomocą interfejsu API sesji już pozwalają na zakończenie pojedynczych sesji.", + "DEBUGOIDCPARENTERROR": "Debug OIDC Błąd nadrzędny", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Jeśli flaga jest włączona, błąd nadrzędny OIDC zostanie zarejestrowany w konsoli.", + "DISABLEUSERTOKENEVENT": "Wyłącz zdarzenie tokena użytkownika", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Włącz Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementuje OpenID Connect Back-Channel Logout 1.0 i może być używany do powiadamiania klientów o zakończeniu sesji u dostawcy OpenID.", + "PERMISSIONCHECKV2": "Sprawdzanie uprawnień V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł korzystać z nowego API i jego funkcji.", + "WEBKEY": "Klucz Web", + "WEBKEY_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł korzystać z nowego API i jego funkcji.", "STATES": { "INHERITED": "Dziedziczony", "ENABLED": "Włączony", @@ -1485,7 +1649,12 @@ "ENABLED": "„Włączony” jest dziedziczone", "DISABLED": "„Wyłączony” jest dziedziczone" }, - "RESET": "Ustaw wszystko na dziedziczone" + "RESET": "Ustaw wszystko na dziedziczone", + "CONSOLEUSEV2USERAPI": "Użyj API V2 w konsoli do tworzenia użytkowników", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Gdy ta flaga jest włączona, konsola używa API V2 User do tworzenia nowych użytkowników. W przypadku API V2 nowo utworzeni użytkownicy rozpoczynają bez stanu początkowego.", + "LOGINV2": "Logowanie V2", + "LOGINV2_DESCRIPTION": "Włączenie tej opcji aktywuje nowy interfejs logowania oparty na TypeScript z ulepszonym bezpieczeństwem, wydajnością i możliwością dostosowania.", + "LOGINV2_BASEURI": "Podstawowy URI" }, "DIALOG": { "RESET": { @@ -1622,7 +1791,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Weryfikacja adresu e-mail zakończona", @@ -2191,7 +2362,9 @@ "REMOVED": "Usunięto pomyślnie." }, "ISIDTOKENMAPPING": "Mapowanie z tokena ID", - "ISIDTOKENMAPPING_DESC": "Jeśli wybrane, informacje dostawcy są mapowane z tokena ID, a nie z punktu końcowego userinfo." + "ISIDTOKENMAPPING_DESC": "Jeśli wybrane, informacje dostawcy są mapowane z tokena ID, a nie z punktu końcowego userinfo.", + "USEPKCE": "Skorzystaj z PKCE", + "USEPKCE_DESC": "Określa, czy parametry code_challenge i code_challenge_method są uwzględnione w żądaniu uwierzytelnienia" }, "MFA": { "LIST": { @@ -2570,7 +2743,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Dodaj managera", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index f6fbc826dd..30b0f1d4e8 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -185,6 +185,32 @@ "DESCRIPTION": "A vida útil do token de atualização inativo é o tempo máximo que um token de atualização pode ficar sem uso." } }, + "WEB_KEYS": { + "DESCRIPTION": "Gerencie suas Chaves Web OIDC para assinar e validar tokens com segurança em sua instância do ZITADEL.", + "TABLE": { + "TITLE": "Chaves Web Ativas e Futuras", + "DESCRIPTION": "Suas chaves web ativas e futuras. Ativar uma nova chave desativará a atual.", + "NOTE": "Nota: O endpoint JWKs OIDC retorna uma resposta que pode ser armazenada em cache (padrão: 5 min). Evite ativar uma chave muito cedo, pois ela pode ainda não estar disponível no cache e para os clientes.", + "ACTIVATE": "Ativar próxima Chave Web", + "ACTIVE": "Atualmente ativa", + "NEXT": "Próxima na fila", + "FUTURE": "Futuro", + "WARNING": "A chave da Web tem menos de 5 minutos" + }, + "CREATE": { + "TITLE": "Criar nova Chave Web", + "DESCRIPTION": "Criar uma nova chave web a adicionará à sua lista. O ZITADEL usa, por padrão, chaves RSA2048 com um algoritmo de hash SHA256.", + "KEY_TYPE": "Tipo de Chave", + "BITS": "Bits", + "HASHER": "Algoritmo de Hash", + "CURVE": "Curva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Chaves Web Anteriores", + "DESCRIPTION": "Estas são suas chaves web anteriores que não estão mais ativas.", + "DEACTIVATED_ON": "Desativada em" + } + }, "MESSAGE_TEXTS": { "TITLE": "Textos de Mensagens", "DESCRIPTION": "Personalize os textos do seu e-mail de notificação ou mensagens SMS. Se desejar desativar alguns idiomas, restrinja-os nas configurações de idioma da sua instância.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Baixar", "APPLY": "Aplicar" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Ações", + "DESCRIPTION": "As ações permitem que você execute código personalizado em resposta a solicitações de API, eventos ou funções específicas. Use-as para estender o Zitadel, automatizar fluxos de trabalho e integrar-se a outros sistemas.", + "TYPES": { + "request": "Solicitação", + "response": "Resposta", + "events": "Eventos", + "function": "Função" + }, + "DIALOG": { + "CREATE_TITLE": "Criar uma Ação", + "UPDATE_TITLE": "Atualizar uma Ação", + "TYPE": { + "DESCRIPTION": "Selecione quando você deseja que esta Ação seja executada", + "REQUEST": { + "TITLE": "Solicitação", + "DESCRIPTION": "Solicitações que ocorrem dentro do Zitadel. Isso pode ser algo como uma chamada de solicitação de login." + }, + "RESPONSE": { + "TITLE": "Resposta", + "DESCRIPTION": "Uma resposta de uma solicitação dentro do Zitadel. Pense na resposta que você recebe ao buscar um usuário." + }, + "EVENTS": { + "TITLE": "Eventos", + "DESCRIPTION": "Eventos que acontecem dentro do Zitadel. Isso pode ser qualquer coisa, como um usuário criando uma conta, um login bem-sucedido, etc." + }, + "FUNCTIONS": { + "TITLE": "Funções", + "DESCRIPTION": "Funções que você pode chamar dentro do Zitadel. Isso pode ser qualquer coisa, desde enviar um e-mail até criar um usuário." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Escolha se esta ação se aplica a todas as solicitações, um serviço específico (por exemplo, gerenciamento de usuários) ou uma única solicitação (por exemplo, criar usuário).", + "ALL": { + "TITLE": "Todas", + "DESCRIPTION": "Selecione isso se você quiser executar sua ação em cada solicitação" + }, + "SELECT_SERVICE": { + "TITLE": "Selecionar Serviço", + "DESCRIPTION": "Escolha um Serviço Zitadel para sua ação." + }, + "SELECT_METHOD": { + "TITLE": "Selecionar Método", + "DESCRIPTION": "Se você quiser executar apenas em uma solicitação específica, selecione-a aqui", + "NOTE": "Se você não selecionar um método, sua ação será executada em todas as solicitações em seu serviço selecionado." + }, + "FUNCTIONNAME": { + "TITLE": "Nome da Função", + "DESCRIPTION": "Escolha a função que você deseja executar" + }, + "SELECT_GROUP": { + "TITLE": "Definir Grupo", + "DESCRIPTION": "Se você quiser executar apenas em um grupo de eventos, defina o grupo aqui" + }, + "SELECT_EVENT": { + "TITLE": "Selecionar Evento", + "DESCRIPTION": "Se você quiser executar apenas em um evento específico, especifique-o aqui" + } + }, + "TARGET": { + "DESCRIPTION": "Você pode escolher executar um destino ou executá-lo nas mesmas condições que outros destinos.", + "TARGET": { + "DESCRIPTION": "O destino que você deseja executar para esta ação" + }, + "CONDITIONS": { + "DESCRIPTION": "Condições de Execução" + } + } + }, + "TABLE": { + "CONDITION": "Condição", + "TYPE": "Tipo", + "TARGET": "Destino", + "CREATIONDATE": "Data de Criação" + } + }, + "TARGET": { + "TITLE": "Destinos", + "DESCRIPTION": "Um destino é o destino do código que você deseja executar a partir de uma ação. Crie um destino aqui e adicione-o às suas ações.", + "CREATE": { + "TITLE": "Criar seu Destino", + "DESCRIPTION": "Crie seu próprio destino fora do Zitadel", + "NAME": "Nome", + "NAME_DESCRIPTION": "Dê ao seu destino um nome claro e descritivo para torná-lo fácil de identificar mais tarde", + "TYPE": "Tipo", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Chamada REST", + "restAsync": "REST Assíncrono" + }, + "ENDPOINT": "Ponto de Extremidade", + "ENDPOINT_DESCRIPTION": "Insira o ponto de extremidade onde seu código está hospedado. Certifique-se de que ele esteja acessível para nós!", + "TIMEOUT": "Tempo Limite", + "TIMEOUT_DESCRIPTION": "Defina o tempo máximo que seu destino tem para responder. Se demorar mais, interromperemos a solicitação.", + "INTERRUPT_ON_ERROR": "Interromper em Caso de Erro", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Pare todas as execuções quando os destinos retornarem um erro", + "INTERRUPT_ON_ERROR_WARNING": "Atenção: “Interromper em caso de erro” interrompe as operações em caso de falha, com risco de bloqueio. Teste com esta opção desativada para evitar bloquear o login/criação.", + "AWAIT_RESPONSE": "Aguardar Resposta", + "AWAIT_RESPONSE_DESCRIPTION": "Aguardaremos uma resposta antes de fazermos qualquer outra coisa. Útil se você pretende usar vários destinos para uma única ação" + }, + "TABLE": { + "NAME": "Nome", + "ENDPOINT": "Ponto de Extremidade", + "CREATIONDATE": "Data de Criação", + "REORDER": "Reordenar" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Tem controle sobre toda a instância, incluindo todas as organizações", "IAM_OWNER_VIEWER": "Tem permissão para revisar toda a instância, incluindo todas as organizações", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Tem permissão para criar e gerenciar usuários", "IAM_ADMIN_IMPERSONATOR": "Tem permissão para se passar por administradores e usuários finais de todas as organizações", "IAM_END_USER_IMPERSONATOR": "Tem permissão para se passar por usuários finais de todas as organizações", + "IAM_LOGIN_CLIENT": "Tem permissão para gerenciar clientes de login", "ORG_OWNER": "Tem permissão sobre toda a organização", "ORG_USER_MANAGER": "Tem permissão para criar e gerenciar usuários da organização", "ORG_OWNER_VIEWER": "Tem permissão para revisar toda a organização", + "ORG_SETTINGS_MANAGER": "Tem permissão para gerenciar as configurações da organização", "ORG_USER_PERMISSION_EDITOR": "Tem permissão para gerenciar concessões de usuários", "ORG_PROJECT_PERMISSION_EDITOR": "Tem permissão para gerenciar concessões de projetos", "ORG_PROJECT_CREATOR": "Tem permissão para criar seus próprios projetos e configurações subjacentes", "ORG_ADMIN_IMPERSONATOR": "Tem permissão para se passar por administradores e usuários finais da organização", "ORG_END_USER_IMPERSONATOR": "Tem permissão para se passar por usuários finais da organização", + "ORG_USER_SELF_MANAGER": "Tem permissão para gerenciar seu próprio usuário", "PROJECT_OWNER": "Tem permissão sobre todo o projeto", "PROJECT_OWNER_VIEWER": "Tem permissão para revisar todo o projeto", "PROJECT_OWNER_GLOBAL": "Tem permissão sobre todo o projeto", @@ -787,7 +925,10 @@ "PHONESECTION": "Números de Telefone", "PASSWORDSECTION": "Senha Inicial", "ADDRESSANDPHONESECTION": "Número de telefone", - "INITMAILDESCRIPTION": "Se ambas as opções forem selecionadas, nenhum e-mail de inicialização será enviado. Se apenas uma das opções for selecionada, um e-mail para fornecer/verificar os dados será enviado." + "INITMAILDESCRIPTION": "Se ambas as opções forem selecionadas, nenhum e-mail de inicialização será enviado. Se apenas uma das opções for selecionada, um e-mail para fornecer/verificar os dados será enviado.", + "SETUPAUTHENTICATIONLATER": "Configurar autenticação mais tarde para este usuário.", + "INVITATION": "Enviar um E-mail de convite para configuração de autenticação e verificação de E-mail.", + "INITIALPASSWORD": "Defina uma senha inicial para o usuário." }, "CODEDIALOG": { "TITLE": "Verificar Número de Telefone", @@ -809,6 +950,9 @@ "EMAIL": "E-mail", "PHONE": "Número de Telefone", "PHONE_HINT": "Use o símbolo + seguido do código de chamada do país, ou selecione o país na lista suspensa e, em seguida, insira o número de telefone", + "PHONE_VERIFIED": "Número de telefone verificado", + "SEND_SMS": "Enviar SMS de verificação", + "SEND_EMAIL": "Enviar E-mail", "USERNAME": "Nome de Usuário", "CHANGEUSERNAME": "modificar", "CHANGEUSERNAME_TITLE": "Alterar nome de usuário", @@ -949,6 +1093,14 @@ "5": "Suspenso", "6": "Inicial" }, + "STATEV2": { + "0": "Desconhecido", + "1": "Ativo", + "2": "Inativo", + "3": "Excluído", + "4": "Bloqueado", + "5": "Inicial" + }, "SEARCH": { "ADDITIONAL": "Nome de usuário (organização atual)", "ADDITIONAL-EXTERNAL": "Nome de usuário (organização externa)" @@ -1341,6 +1493,7 @@ "BRANDING": "Marca", "PRIVACYPOLICY": "Política de Privacidade", "OIDC": "Tempo de Vida e Expiração do Token OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Aparência de Segredo", "SECURITY": "Configurações de Segurança", "EVENTS": "Eventos", @@ -1386,7 +1539,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1477,6 +1631,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 permitem gerenciar execuções e destinos de dados. Se a flag estiver habilitada, você poderá usar a nova API e seus recursos.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Término de sessão", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Se a bandeira estiver habilitada, você poderá encerrar uma sessão única da interface do usuário de login fornecendo um id_token com uma reivindicação `sid como id_token_hint no ponto final de end_session. Observe que atualmente todas as sessões do mesmo agente de usuário (navegador) são encerradas na interface do usuário de login. As sessões gerenciadas por meio da API de sessão já permitem o encerramento de sessões individuais.", + "DEBUGOIDCPARENTERROR": "Erro de Depuração do Pai OIDC", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Se a bandeira estiver ativada, o erro do pai OIDC será registrado no console.", + "DISABLEUSERTOKENEVENT": "Desativar Evento de Token de Usuário", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Ativar Logout de Backchannel", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "O Logout de Back-Channel implementa o OpenID Connect Back-Channel Logout 1.0 e pode ser usado para notificar os clientes sobre a terminação da sessão no Provedor de OpenID.", + "PERMISSIONCHECKV2": "Verificação de Permissão V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Se a bandeira estiver ativada, você poderá usar a nova API e seus recursos.", + "WEBKEY": "Chave Web", + "WEBKEY_DESCRIPTION": "Se a bandeira estiver ativada, você poderá usar a nova API e seus recursos.", "STATES": { "INHERITED": "Herdade", "ENABLED": "Habilitado", @@ -1487,7 +1651,12 @@ "ENABLED": "\"Habilitado\" é herdado", "DISABLED": "\"Desabilitado\" é herdado" }, - "RESET": "Definir tudo para herdar" + "RESET": "Definir tudo para herdar", + "CONSOLEUSEV2USERAPI": "Use a API V2 no console para criação de usuários", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Quando esta opção está ativada, o console utiliza a API V2 de Usuários para criar novos usuários. Com a API V2, os novos usuários criados começam sem um estado inicial.", + "LOGINV2": "Login V2", + "LOGINV2_DESCRIPTION": "Ativar esta opção ativa a nova interface de login baseada em TypeScript, com melhorias na segurança, desempenho e personalização.", + "LOGINV2_BASEURI": "URI base" }, "DIALOG": { "RESET": { @@ -1624,7 +1793,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "Verificação de email concluída", @@ -2187,7 +2358,9 @@ "REMOVED": "Removido com sucesso." }, "ISIDTOKENMAPPING": "Mapeamento do token ID", - "ISIDTOKENMAPPING_DESC": "Se selecionado, as informações do provedor são mapeadas a partir do token ID, e não do ponto final userinfo." + "ISIDTOKENMAPPING_DESC": "Se selecionado, as informações do provedor são mapeadas a partir do token ID, e não do ponto final userinfo.", + "USEPKCE": "Usar PKCE", + "USEPKCE_DESC": "Determina se os parâmetros code_challenge e code_challenge_method estão incluídos na solicitação de autenticação" }, "MFA": { "LIST": { @@ -2566,7 +2739,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Adicionar um Gerente", diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json new file mode 100644 index 0000000000..6c73852beb --- /dev/null +++ b/console/src/assets/i18n/ro.json @@ -0,0 +1,2846 @@ +{ + "APP_NAME": "ZITADEL", + "DESCRIPTIONS": { + "METADATA_TITLE": "Metadata", + "HOME": { + "TITLE": "Începeți cu ZITADEL", + "NEXT": { + "TITLE": "Pașii următori", + "DESCRIPTION": "Finalizați următorii pași pentru a vă securiza aplicația.", + "CREATE_PROJECT": { + "TITLE": "Creați un proiect", + "DESCRIPTION": "Adăugați un proiect și definiți rolurile și autorizațiile acestuia." + } + }, + "MORE_SHORTCUTS": { + "GET_STARTED": { + "TITLE": "Începeți", + "DESCRIPTION": "Urmați ghidul rapid pas cu pas și începeți să construiți imediat." + }, + "DOCS": { + "TITLE": "Documentație", + "DESCRIPTION": "Explorați baza de cunoștințe ZITADEL pentru a vă familiariza cu conceptele și ideile de bază. Aflați cum funcționează ZITADEL și cum să îl utilizați." + }, + "EXAMPLES": { + "TITLE": "Exemple și Kituri de Dezvoltare Software", + "DESCRIPTION": "Navigați prin exemplele și SDK-urile noastre pentru a utiliza ZITADEL împreună cu limbajele și instrumentele de programare preferate." + } + } + }, + "ORG": { + "TITLE": "Organizație", + "DESCRIPTION": "O organizație găzduiește utilizatori, proiecte cu aplicații, furnizori de identitate și setări precum brandingul companiei. Doriți să partajați setări între mai multe organizații? Configurați setările implicite.", + "METADATA": "Adăugați atribute personalizate organizației, cum ar fi locația sau un identificator într-un alt sistem. Puteți utiliza aceste informații în acțiunile dvs." + }, + "PROJECTS": { + "TITLE": "Proiecte", + "DESCRIPTION": "Un proiect găzduiește una sau mai multe aplicații, pe care le puteți utiliza pentru a vă autentifica utilizatorii. De asemenea, vă puteți autoriza utilizatorii cu proiecte. Pentru a permite utilizatorilor din alte organizații să se conecteze la aplicațiile dvs., acordați-le acces la proiectul dvs.

Dacă nu găsiți un proiect, contactați proprietarul proiectelor sau pe cineva cu drepturile corespunzătoare pentru a obține acces.", + "OWNED": { + "TITLE": "Proiecte deținute", + "DESCRIPTION": "Acestea sunt proiectele pe care le dețineți. Puteți gestiona setările acestor proiecte, autorizațiile și aplicațiile." + }, + "GRANTED": { + "TITLE": "Proiecte acordate", + "DESCRIPTION": "Acestea sunt proiectele pe care alte organizații vi le-au acordat. Cu proiectele acordate, puteți oferi utilizatorilor dvs. acces la aplicațiile altor organizații." + } + }, + "USERS": { + "TITLE": "Utilizatori", + "DESCRIPTION": "Un utilizator este o persoană sau o mașină care vă poate accesa aplicațiile.", + "HUMANS": { + "TITLE": "Utilizatori", + "DESCRIPTION": "Utilizatorii se autentifică interactiv într-o sesiune de browser cu o solicitare de autentificare.", + "METADATA": "Adăugați atribute personalizate utilizatorului, cum ar fi departamentul. Puteți utiliza aceste informații în acțiunile dvs." + }, + "MACHINES": { + "TITLE": "Utilizatori de servicii", + "DESCRIPTION": "Utilizatorii de servicii se autentifică non-interactiv folosind un token JWT bearer semnat cu o cheie privată. De asemenea, pot utiliza un token de acces personal.", + "METADATA": "Adăugați atribute personalizate utilizatorului, cum ar fi sistemul de autentificare. Puteți utiliza aceste informații în acțiunile dvs." + }, + "SELF": { + "METADATA": "Adăugați atribute personalizate utilizatorului dvs., cum ar fi departamentul. Puteți utiliza aceste informații în acțiunile organizațiilor dvs." + } + }, + "AUTHORIZATIONS": { + "TITLE": "Autorizații", + "DESCRIPTION": "Autorizațiile definesc drepturile de acces ale unui utilizator la un proiect. Puteți acorda unui utilizator acces la un proiect și puteți defini rolurile utilizatorilor în cadrul acelui proiect." + }, + "ACTIONS": { + "TITLE": "Acțiuni", + "DESCRIPTION": "Executați cod personalizat pe evenimente care se întâmplă pe măsură ce utilizatorii dvs. se autentifică la ZITADEL. Automatizați-vă procesele, îmbogățiți metadatele utilizatorilor și tokenurile lor sau notificați sistemele externe.", + "SCRIPTS": { + "TITLE": "Scripturi", + "DESCRIPTION": "Scrieți codul JavaScript o singură dată și declanșați-l în mai multe fluxuri." + }, + "FLOWS": { + "TITLE": "Fluxuri", + "DESCRIPTION": "Alegeți un flux de autentificare și declanșați acțiunea dvs. la un anumit eveniment din cadrul acestui flux." + } + }, + "SETTINGS": { + "INSTANCE": { + "TITLE": "Setări implicite", + "DESCRIPTION": "Setări implicite pentru toate organizațiile. Cu permisiunile potrivite, unele dintre ele pot fi suprascrise în setările organizației." + }, + "ORG": { + "TITLE": "Setări organizație", + "DESCRIPTION": "Personalizați setările organizației dvs." + }, + "FEATURES": { + "TITLE": "Setări caracteristici", + "DESCRIPTION": "Deblocați caracteristici pentru instanța dvs." + }, + "IDPS": { + "TITLE": "Furnizori de identitate", + "DESCRIPTION": "Creați și activați furnizori de identitate externi. Alegeți un furnizor bine-cunoscut sau configurați orice alt furnizor compatibil OIDC, OAuth sau SAML la alegere. Puteți chiar să utilizați tokenurile JWT existente ca identități federate prin configurarea unui furnizor de identitate JWT.", + "NEXT": "Ce urmează?", + "SAML": { + "TITLE": "Configurați-vă furnizorul de identitate SAML", + "DESCRIPTION": "ZITADEL este configurat. Acum, furnizorul dvs. de identitate SAML are nevoie de o anumită configurație. Majoritatea furnizorilor vă permit să încărcați pur și simplu întregul XML de metadate ZITADEL. Alți furnizori vă cer să furnizați doar câteva URL-uri distincte, cum ar fi, de exemplu, ID-ul entității (URL-ul metadatelor), URL-ul Assertion Consumer Service (ACS) sau URL-ul Single Logout." + }, + "CALLBACK": { + "TITLE": "Configurați-vă furnizorul de identitate {{ provider }}", + "DESCRIPTION": "Înainte de a putea configura ZITADEL, transmiteți acest URL furnizorului dvs. de identitate pentru a activa redirecționarea browserului înapoi la ZITADEL după autentificare." + }, + "JWT": { + "TITLE": "Utilizați JWT-urile ca identități federate", + "DESCRIPTION": "Furnizorul de identitate JWT vă permite să utilizați tokenurile JWT existente ca identități federate. Această caracteristică este utilă dacă aveți deja un emitent pentru JWT-uri. Cu un IdP JWT, puteți utiliza aceste JWT-uri pentru a crea și actualiza utilizatori în ZITADEL din mers." + }, + "LDAP": { + "TITLE": "Configurați ZITADEL pentru a vă conecta la furnizorul dvs. de identitate LDAP", + "DESCRIPTION": "Furnizați detaliile de conectare la serverul dvs. LDAP și configurați maparea atributelor LDAP la atributele ZITADEL." + }, + "AUTOFILL": { + "TITLE": "Completare automată a datelor utilizatorului", + "DESCRIPTION": "Utilizați o acțiune pentru a îmbunătăți experiența utilizatorilor dvs. Puteți pre-completa formularul de înregistrare ZITADEL cu valori de la furnizorul de identitate." + }, + "ACTIVATE": { + "TITLE": "Activați IdP-ul", + "DESCRIPTION": "IdP-ul dvs. nu este încă activ. Activați-l pentru a permite utilizatorilor să se conecteze." + } + }, + "PW_COMPLEXITY": { + "TITLE": "Complexitatea parolei", + "DESCRIPTION": "Asigurați-vă că utilizatorii dvs. utilizează parole puternice definind reguli de complexitate." + }, + "BRANDING": { + "TITLE": "Branding", + "DESCRIPTION": "Personalizați aspectul formularului dvs. de autentificare. Nu uitați să aplicați configurația dvs. când ați terminat." + }, + "PRIVACY_POLICY": { + "TITLE": "Linkuri externe", + "DESCRIPTION": "Ghidați utilizatorii către resurse externe personalizate afișate pe pagina de autentificare. Utilizatorii trebuie să accepte Termenii și condițiile și Politica de confidențialitate înainte de a se putea înregistra. Modificați linkul către documentația dvs. sau setați un șir gol pentru a ascunde butonul de documentație din consolă. Adăugați un link extern personalizat și un text personalizat pentru acel link în consolă sau setați-le goale pentru a ascunde acel buton." + }, + "SMTP_PROVIDER": { + "TITLE": "Setări SMTP", + "DESCRIPTION": "Configurați serverul dvs. SMTP pentru a utiliza un domeniu pentru adresa expeditorului pe care utilizatorii dvs. o cunosc și au încredere." + }, + "SMS_PROVIDER": { + "TITLE": "Setări SMS", + "DESCRIPTION": "Pentru a debloca toate caracteristicile ZITADEL, configurați Twilio pentru a trimite mesaje SMS utilizatorilor dvs." + }, + "IAM_EVENTS": { + "TITLE": "Evenimente", + "DESCRIPTION": "Această pagină afișează toate modificările de stare din instanța dvs., până la limita de urmărire de audit a instanțelor dvs. Filtrați lista după intervalul de timp în scopuri de depanare sau filtrați-o după un agregat în scopuri de audit." + }, + "IAM_FAILED_EVENTS": { + "TITLE": "Evenimente eșuate", + "DESCRIPTION": "Această pagină afișează toate evenimentele eșuate din instanța dvs. Dacă ZITADEL nu se comportă așa cum vă așteptați, verificați întotdeauna mai întâi această listă." + }, + "IAM_VIEWS": { + "TITLE": "Vizualizări", + "DESCRIPTION": "Această pagină afișează toate vizualizările bazei dvs. de date și când au procesat cel mai recent eveniment. Dacă vă lipsesc date, verificați dacă vizualizarea este actualizată." + }, + "LANGUAGES": { + "TITLE": "Limbi", + "DESCRIPTION": "Restricționați limbile în care formularul de autentificare și mesajele de notificare sunt traduse. Dacă doriți să dezactivați unele dintre limbi, trageți-le în secțiunea Limbi nepermise. Puteți specifica o limbă permisă ca limbă implicită. Dacă limba preferată a utilizatorilor nu este permisă, se utilizează limba implicită." + }, + "SECRET_GENERATORS": { + "TITLE": "Generatoare de secrete", + "DESCRIPTION": "Definiți complexitățile și duratele de viață ale secretelor dvs. O complexitate și o durată de viață mai mari îmbunătățesc securitatea, o complexitate și o durată de viață mai mici îmbunătățesc performanța de decriptare." + }, + "SECURITY": { + "TITLE": "Setări de securitate", + "DESCRIPTION": "Activați caracteristicile ZITADEL care pot avea impact asupra securității. Ar trebui să știți cu adevărat ce faceți înainte de a modifica aceste setări." + }, + "OIDC": { + "TITLE": "Setări OpenID Connect", + "DESCRIPTION": "Configurați duratele de viață ale tokenurilor OIDC. Utilizați durate de viață mai scurte pentru a crește securitatea utilizatorilor dvs., utilizați durate de viață mai lungi pentru a crește confortul utilizatorilor dvs.", + "LABEL_HOURS": "Durata maximă de viață în ore", + "LABEL_DAYS": "Durata maximă de viață în zile", + "ACCESS_TOKEN": { + "TITLE": "Token de acces", + "DESCRIPTION": "Tokenul de acces este utilizat pentru a autentifica un utilizator. Este un token de scurtă durată care este utilizat pentru a accesa datele utilizatorului. Utilizați o durată de viață scurtă pentru a minimiza riscul de acces neautorizat. Tokenurile de acces pot fi reîmprospătate automat utilizând un token de reîmprospătare." + }, + "ID_TOKEN": { + "TITLE": "Token ID", + "DESCRIPTION": "Tokenul ID este un JSON Web Token (JWT) care conține informații despre utilizator. Durata de viață a tokenului ID nu trebuie să depășească durata de viață a tokenului de acces." + }, + "REFRESH_TOKEN": { + "TITLE": "Token de reîmprospătare", + "DESCRIPTION": "Tokenul de reîmprospătare este utilizat pentru a obține un token de acces nou. Este un token de lungă durată care este utilizat pentru a reîmprospăta tokenul de acces. Un utilizator trebuie să se re-autentifice manual când tokenul de reîmprospătare expiră." + }, + "REFRESH_TOKEN_IDLE": { + "TITLE": "Token de reîmprospătare inactiv", + "DESCRIPTION": "Durata de viață inactivă a tokenului de reîmprospătare este timpul maxim în care un token de reîmprospătare poate fi neutilizat." + } + }, + "WEB_KEYS": { + "DESCRIPTION": "Gestionează-ți cheile web OIDC pentru a semna și valida în siguranță tokenurile pentru instanța ta ZITADEL.", + "TABLE": { + "TITLE": "Chei Web Active și Viitoare", + "DESCRIPTION": "Cheile tale web active și viitoare. Activarea unei noi chei va dezactiva cheia curentă.", + "NOTE": "Notă: Endpoint-ul JWKs OIDC returnează un răspuns care poate fi stocat în cache (implicit 5 min). Evită activarea unei chei prea devreme, deoarece este posibil să nu fie încă disponibilă în cache și pentru clienți.", + "ACTIVATE": "Activează următoarea Cheie Web", + "ACTIVE": "În prezent activă", + "NEXT": "Următoarea în coadă", + "FUTURE": "Viitoare", + "WARNING": "Cheia web are mai puțin de 5 minute" + }, + "CREATE": { + "TITLE": "Creează o nouă Cheie Web", + "DESCRIPTION": "Crearea unei noi chei web o va adăuga pe lista ta. ZITADEL folosește implicit chei RSA2048 cu un algoritm de hash SHA256.", + "KEY_TYPE": "Tip de Cheie", + "BITS": "Biti", + "HASHER": "Algoritm de Hash", + "CURVE": "Curbă" + }, + "PREVIOUS_TABLE": { + "TITLE": "Chei Web Anterioare", + "DESCRIPTION": "Acestea sunt cheile tale web anterioare care nu mai sunt active.", + "DEACTIVATED_ON": "Dezactivată pe" + } + }, + "MESSAGE_TEXTS": { + "TITLE": "Texte de mesaje", + "DESCRIPTION": "Personalizați textele mesajelor de e-mail sau SMS de notificare. Dacă doriți să dezactivați unele dintre limbi, restricționați-le în setările de limbă ale instanțelor dvs.", + "TYPE_DESCRIPTIONS": { + "DC": "Când revendicați un domeniu pentru organizația dvs., utilizatorii care nu utilizează acest domeniu în numele lor de conectare vor fi solicitați să își schimbe numele de conectare pentru a se potrivi cu domeniul revendicat.", + "INIT": "Când un utilizator este creat, acesta va primi un e-mail cu un link pentru a-și seta parola.", + "PC": "Când un utilizator își schimbă parola, acesta va primi o notificare despre modificare dacă ați activat acest lucru în setările de notificare.", + "PL": "Când un utilizator adaugă o metodă de autentificare fără parolă, acesta trebuie să o activeze făcând clic pe un link dintr-un e-mail.", + "PR": "Când un utilizator își resetează parola, acesta va primi un e-mail cu un link pentru a seta o parolă nouă.", + "VE": "Când un utilizator își schimbă adresa de e-mail, acesta va primi un e-mail cu un link pentru a verifica noua adresă.", + "VP": "Când un utilizator își schimbă numărul de telefon, acesta va primi un SMS cu un cod pentru a verifica noul număr.", + "VEO": "Când un utilizator adaugă o parolă unică prin metoda e-mail, acesta trebuie să o activeze introducând un cod trimis la adresa sa de e-mail.", + "VSO": "Când un utilizator adaugă o parolă unică prin metoda SMS, acesta trebuie să o activeze introducând un cod trimis la numărul său de telefon.", + "IU": "Când este creat un cod de invitație pentru utilizator, acesta va primi un e-mail cu un link pentru a-și seta metoda de autentificare." + } + }, + "LOGIN_TEXTS": { + "TITLE": "Texte interfață de autentificare", + "DESCRIPTION": "Personalizați textele formularului dvs. de autentificare. Dacă un text este gol, substituentul afișează valoarea implicită. Dacă doriți să dezactivați unele dintre limbi, restricționați-le în setările de limbă ale instanțelor dvs." + }, + "DOMAINS": { + "TITLE": "Setări domeniu", + "DESCRIPTION": "Definiți restricții asupra domeniilor dvs. și configurați modelele de nume de conectare.", + "REQUIRE_VERIFICATION": { + "TITLE": "Solicitați ca domeniile personalizate să fie verificate", + "DESCRIPTION": "Dacă acest lucru este activat, domeniile organizației trebuie să fie verificate înainte de a putea fi utilizate pentru descoperirea domeniului sau sufixarea numelui de utilizator." + }, + "LOGIN_NAME_PATTERN": { + "TITLE": "Model de nume de conectare", + "DESCRIPTION": "Controlați modelul numelor de conectare ale utilizatorilor dvs. ZITADEL selectează organizația utilizatorilor dvs. imediat ce aceștia își introduc numele de conectare. Prin urmare, numele de conectare trebuie să fie unice în toate organizațiile. Dacă aveți utilizatori care au un cont în mai multe domenii, puteți asigura unicitatea prin sufixarea numelor de conectare cu domeniul organizației." + }, + "DOMAIN_VERIFICATION": { + "TITLE": "Verificarea domeniului", + "DESCRIPTION": "Permiteți organizației dvs. să utilizeze numai domeniile pe care le controlează efectiv. Dacă este activată, domeniile organizației sunt verificate periodic prin DNS sau provocare HTTP înainte de a putea fi utilizate. Aceasta este o caracteristică de securitate pentru a preveni deturnarea domeniului." + }, + "SMTP_SENDER_ADDRESS": { + "TITLE": "Adresa expeditorului SMTP", + "DESCRIPTION": "Permiteți o adresă expeditorului SMTP numai dacă se potrivește cu unul dintre domeniile instanței dvs." + } + }, + "LOGIN": { + "LIFETIMES": { + "TITLE": "Durate de viață autentificare", + "DESCRIPTION": "Consolidați-vă securitatea prin reducerea unor durate maxime de viață legate de autentificare.", + "LABEL": "Durata maximă de viață în ore", + "PW_CHECK": { + "TITLE": "Verificare parolă", + "DESCRIPTION": "Utilizatorii vor trebui să se re-autentifice cu parola lor după această perioadă." + }, + "EXT_LOGIN_CHECK": { + "TITLE": "Verificare autentificare externă", + "DESCRIPTION": "Utilizatorii dvs. sunt redirecționați către furnizorii lor de identitate externi după aceste perioade." + }, + "MULTI_FACTOR_INIT": { + "TITLE": "Verificare inițializare multifactor", + "DESCRIPTION": "Utilizatorii dvs. vor fi solicitați să configureze un al doilea factor sau un Multifactor după aceste perioade, dacă nu au făcut-o deja. O durată de viață de 0 dezactivează această solicitare." + }, + "SECOND_FACTOR_CHECK": { + "TITLE": "Verificare al doilea factor", + "DESCRIPTION": "Utilizatorii dvs. trebuie să își revalideze al doilea factor în aceste perioade." + }, + "MULTI_FACTOR_CHECK": { + "TITLE": "Verificare multifactor", + "DESCRIPTION": "Utilizatorii dvs. trebuie să își revalideze multifactorul în aceste perioade." + } + }, + "FORM": { + "TITLE": "Formular de autentificare", + "DESCRIPTION": "Personalizați formularul de autentificare.", + "USERNAME_PASSWORD_ALLOWED": { + "TITLE": "Nume de utilizator și parolă permise", + "DESCRIPTION": "Permiteți utilizatorilor dvs. să se conecteze cu numele lor de utilizator și parola. Dacă acest lucru este dezactivat, utilizatorii dvs. se pot conecta numai utilizând autentificarea fără parolă sau cu un furnizor de identitate extern." + }, + "USER_REGISTRATION_ALLOWED": { + "TITLE": "Înregistrarea utilizatorilor permisă", + "DESCRIPTION": "Permiteți utilizatorilor anonimi să își creeze un cont." + }, + "ORG_REGISTRATION_ALLOWED": { + "TITLE": "Înregistrarea organizației permisă", + "DESCRIPTION": "Permiteți utilizatorilor anonimi să își creeze o organizație." + }, + "EXTERNAL_LOGIN_ALLOWED": { + "TITLE": "Autentificare externă permisă", + "DESCRIPTION": "Permiteți utilizatorilor dvs. să se conecteze cu un furnizor de identitate extern în loc să utilizeze utilizatorul ZITADEL pentru a se conecta." + }, + "HIDE_PASSWORD_RESET": { + "TITLE": "Resetarea parolei ascunsă", + "DESCRIPTION": "Nu permiteți utilizatorilor dvs. să își reseteze parola." + }, + "DOMAIN_DISCOVERY_ALLOWED": { + "TITLE": "Descoperirea domeniului permisă", + "DESCRIPTION": "Găsiți organizațiile utilizatorilor dvs. în funcție de domeniul numelor lor de conectare, de exemplu, adresa lor de e-mail." + }, + "IGNORE_UNKNOWN_USERNAMES": { + "TITLE": "Ignorați numele de utilizator necunoscute", + "DESCRIPTION": "Dacă acest lucru este activat, formularul de autentificare nu va afișa un mesaj de eroare dacă numele de utilizator este necunoscut. Acest lucru ajută la prevenirea ghicirii numelui de utilizator." + }, + "DISABLE_EMAIL_LOGIN": { + "TITLE": "Dezactivați autentificarea prin e-mail", + "DESCRIPTION": "Dacă acest lucru este activat, utilizatorii dvs. nu își pot utiliza adresele de e-mail pentru a se conecta. Atenție, dacă dezactivați acest lucru, adresele de e-mail ale utilizatorilor dvs. trebuie să fie unice în toate organizațiile pentru a se putea conecta." + }, + "DISABLE_PHONE_LOGIN": { + "TITLE": "Dezactivați autentificarea prin telefon", + "DESCRIPTION": "Dacă acest lucru este activat, utilizatorii dvs. nu își pot utiliza numerele de telefon pentru a se conecta. Atenție, dacă dezactivați acest lucru, numerele de telefon ale utilizatorilor dvs. trebuie să fie unice în toate organizațiile pentru a se putea conecta." + } + } + } + } + }, + "PAGINATOR": { + "PREVIOUS": "Anterior", + "NEXT": "Următorul", + "COUNT": "Rezultate totale", + "MORE": "Mai multe" + }, + "FOOTER": { + "LINKS": { + "CONTACT": "Contact", + "TOS": "Termeni și condiții", + "PP": "Politica de confidențialitate" + }, + "THEME": { + "DARK": "Întunecat", + "LIGHT": "Deschis" + } + }, + "HOME": { + "WELCOME": "Începeți cu ZITADEL", + "DISCLAIMER": "ZITADEL vă tratează datele în mod confidențial și sigur.", + "DISCLAIMERLINK": "Informații suplimentare", + "DOCUMENTATION": { + "DESCRIPTION": "Începeți rapid cu ZITADEL." + }, + "GETSTARTED": { + "DESCRIPTION": "Începeți rapid cu ZITADEL." + }, + "QUICKSTARTS": { + "LABEL": "Primii pași", + "DESCRIPTION": "Începeți rapid cu ZITADEL." + }, + "SHORTCUTS": { + "SHORTCUTS": "Comenzi rapide", + "SETTINGS": "Comenzi rapide disponibile", + "PROJECTS": "Proiecte", + "REORDER": "Țineți apăsat și trageți țigla pentru a o muta", + "ADD": "Țineți apăsat și trageți o țiglă pentru a o adăuga" + } + }, + "ONBOARDING": { + "DESCRIPTION": "Pașii următori", + "MOREDESCRIPTION": "mai multe comenzi rapide", + "COMPLETED": "finalizat", + "DISMISS": "Nu, mulțumesc, sunt profesionist.", + "CARD": { + "TITLE": "Puneți ZITADEL să funcționeze", + "DESCRIPTION": "Această listă de verificare ajută la configurarea instanței și vă ghidează prin cei mai esențiali pași" + }, + "MILESTONES": { + "instance.policy.label.added": { + "title": "Configurați-vă brandul", + "description": "Definiți culoarea și forma autentificării și încărcați logo-ul și pictogramele.", + "action": "Configurați brandingul" + }, + "instance.smtp.config.added": { + "title": "Configurați-vă setările SMTP", + "description": "Setați propriile setări ale serverului de e-mail.", + "action": "Configurați SMTP" + }, + "PROJECT_CREATED": { + "title": "Creați un proiect", + "description": "Adăugați un proiect și definiți rolurile și autorizațiile acestuia.", + "action": "Creați proiect" + }, + "APPLICATION_CREATED": { + "title": "Înregistrați-vă aplicația", + "description": "Înregistrați-vă aplicația web, nativă, api sau saml și configurați un flux de autentificare.", + "action": "Înregistrați aplicația" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Conectați-vă la aplicația dvs.", + "description": "Integrați aplicația dvs. cu ZITADEL pentru autentificare și testați-o conectându-vă cu utilizatorul dvs. administrator.", + "action": "Conectați-vă" + }, + "user.human.added": { + "title": "Adăugați utilizatori", + "description": "Adăugați utilizatorii aplicației dvs.", + "action": "Adăugați utilizator" + }, + "user.grant.added": { + "title": "Acordați utilizatorilor", + "description": "Permiteți utilizatorilor să acceseze aplicația dvs. și să își configureze rolul.", + "action": "Acordați utilizatorului" + } + } + }, + "MENU": { + "INSTANCE": "Setări implicite", + "DASHBOARD": "Acasă", + "PERSONAL_INFO": "Informații personale", + "DOCUMENTATION": "Documentație", + "INSTANCEOVERVIEW": "Instanță", + "ORGS": "Organizații", + "VIEWS": "Vizualizări", + "EVENTS": "Evenimente", + "FAILEDEVENTS": "Evenimente eșuate", + "ORGANIZATION": "Organizație", + "PROJECT": "Proiecte", + "PROJECTOVERVIEW": "Prezentare generală", + "PROJECTGRANTS": "Granturi", + "ROLES": "Roluri", + "GRANTEDPROJECT": "Proiecte acordate", + "HUMANUSERS": "Utilizatori", + "MACHINEUSERS": "Utilizatori de servicii", + "LOGOUT": "Deconectați toți utilizatorii", + "NEWORG": "Organizație nouă", + "IAMADMIN": "Sunteți administrator IAM. Rețineți că aveți permisiuni extinse.", + "SHOWORGS": "Afișați toate organizațiile", + "GRANTS": "Autorizații", + "ACTIONS": "Acțiuni", + "PRIVACY": "Confidențialitate", + "TOS": "Termeni și condiții", + "OPENSHORTCUTSTOOLTIP": "Tastați ? pentru a afișa comenzile rapide de la tastatură", + "SETTINGS": "Setări", + "CUSTOMERPORTAL": "Portalul clienților" + }, + "QUICKSTART": { + "TITLE": "Integrați ZITADEL în aplicația dvs.", + "DESCRIPTION": "Integrați ZITADEL în aplicația dvs. sau utilizați unul dintre exemplele noastre pentru a începe în câteva minute.", + "BTN_START": "Creați aplicație", + "BTN_LEARNMORE": "Aflați mai multe", + "CREATEPROJECTFORAPP": "Creați proiectul {{value}}", + "SELECT_FRAMEWORK": "Selectați Framework", + "FRAMEWORK": "Framework", + "FRAMEWORK_OTHER": "Altele (OIDC, SAML, API)", + "ALMOSTDONE": "Aproape ați terminat.", + "REVIEWCONFIGURATION": "Revizuiți configurația", + "REVIEWCONFIGURATION_DESCRIPTION": "Am creat o configurație de bază pentru aplicațiile {{value}}. Puteți adapta această configurație nevoilor dvs. după creare.", + "REDIRECTS": "Configurați redirecționări", + "DEVMODEWARN": "Modul Dev este activat în mod implicit. Puteți actualiza valorile pentru producție mai târziu.", + "GUIDE": "Ghid", + "BROWSEEXAMPLES": "Răsfoiți Exemple și SDK-uri", + "DUPLICATEAPPRENAME": "Există deja o aplicație cu același nume. Vă rugăm să alegeți un alt nume.", + "DIALOG": { + "CHANGE": { + "TITLE": "Modificați Framework-ul", + "DESCRIPTION": "Alegeți unul dintre framework-urile disponibile pentru configurarea rapidă a aplicației dvs." + } + } + }, + "ACTIONS": { + "ACTIONS": "Acțiuni", + "FILTER": "Filtru", + "RENAME": "Redenumiți", + "SET": "Setați", + "COPY": "Copiați în clipboard", + "COPIED": "Copiat în clipboard.", + "RESET": "Resetați", + "RESETDEFAULT": "Resetați la implicit", + "RESETTO": "Resetați la:", + "RESETCURRENT": "Resetați la curent", + "SHOW": "Afișați", + "HIDE": "Ascundeți", + "SAVE": "Salvați", + "SAVENOW": "Salvați acum", + "NEW": "Nou", + "ADD": "Adăugați", + "CREATE": "Creați", + "CONTINUE": "Continuați", + "CONTINUEWITH": "Continuați cu {{value}}", + "BACK": "Înapoi", + "CLOSE": "Închideți", + "CLEAR": "Ștergeți", + "CANCEL": "Anulați", + "INFO": "Info", + "OK": "OK", + "SELECT": "Selectați", + "VIEW": "Afișați", + "SELECTIONDELETE": "Ștergeți selecția", + "DELETE": "Ștergeți", + "REMOVE": "Eliminați", + "VERIFY": "Verificați", + "FINISH": "Finalizați", + "FINISHED": "Închideți", + "CHANGE": "Modificați", + "REACTIVATE": "Reactivați", + "ACTIVATE": "Activați", + "DEACTIVATE": "Dezactivați", + "REFRESH": "Reîmprospătați", + "LOGIN": "Conectați-vă", + "EDIT": "Editați", + "PIN": "Fixați / Anulați fixarea", + "CONFIGURE": "Configurați", + "SEND": "Trimiteți", + "NEWVALUE": "Valoare nouă", + "RESTORE": "Restabiliți", + "CONTINUEWITHOUTSAVE": "Continuați fără a salva", + "OF": "din", + "PREVIOUS": "Anterior", + "NEXT": "Următorul", + "MORE": "mai multe", + "STEP": "Pasul", + "SETUP": "Configurați", + "TEST": "Testați", + "UNSAVEDCHANGES": "Modificări nesalvate", + "UNSAVED": { + "DIALOG": { + "DESCRIPTION": "Sigur doriți să renunțați la această acțiune nouă? Acțiunea dvs. se va pierde", + "CANCEL": "Anulați", + "DISCARD": "Renunțați" + } + }, + "TABLE": { + "SHOWUSER": "Afișați utilizatorul {{value}}" + }, + "DOWNLOAD": "Descărcați", + "APPLY": "Aplicați" + }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Acțiuni", + "DESCRIPTION": "Acțiunile vă permit să rulați cod personalizat ca răspuns la cereri API, evenimente sau funcții specifice. Folosiți-le pentru a extinde Zitadel, a automatiza fluxurile de lucru și a vă integra cu alte sisteme.", + "TYPES": { + "request": "Cerere", + "response": "Răspuns", + "events": "Evenimente", + "function": "Funcție" + }, + "DIALOG": { + "CREATE_TITLE": "Creează o Acțiune", + "UPDATE_TITLE": "Actualizează o Acțiune", + "TYPE": { + "DESCRIPTION": "Selectați când doriți să rulați această Acțiune", + "REQUEST": { + "TITLE": "Cerere", + "DESCRIPTION": "Cereri care apar în Zitadel. Acesta ar putea fi ceva de genul unui apel de cerere de autentificare." + }, + "RESPONSE": { + "TITLE": "Răspuns", + "DESCRIPTION": "Un răspuns la o cerere în Zitadel. Gândiți-vă la răspunsul pe care îl primiți la preluarea unui utilizator." + }, + "EVENTS": { + "TITLE": "Evenimente", + "DESCRIPTION": "Evenimente care se întâmplă în Zitadel. Acesta ar putea fi orice, cum ar fi un utilizator care își creează un cont, o autentificare reușită etc." + }, + "FUNCTIONS": { + "TITLE": "Funcții", + "DESCRIPTION": "Funcții pe care le puteți apela în Zitadel. Acesta ar putea fi orice, de la trimiterea unui e-mail la crearea unui utilizator." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Alegeți dacă această acțiune se aplică tuturor cererilor, unui serviciu specific (de exemplu, gestionarea utilizatorilor) sau unei singure cereri (de exemplu, crearea unui utilizator).", + "ALL": { + "TITLE": "Toate", + "DESCRIPTION": "Selectați aceasta dacă doriți să rulați acțiunea la fiecare cerere" + }, + "SELECT_SERVICE": { + "TITLE": "Selectați Serviciul", + "DESCRIPTION": "Alegeți un Serviciu Zitadel pentru acțiunea dvs." + }, + "SELECT_METHOD": { + "TITLE": "Selectați Metoda", + "DESCRIPTION": "Dacă doriți să rulați numai la o cerere specifică, selectați-o aici", + "NOTE": "Dacă nu selectați o metodă, acțiunea dvs. va rula la fiecare cerere din serviciul selectat." + }, + "FUNCTIONNAME": { + "TITLE": "Numele Funcției", + "DESCRIPTION": "Alegeți funcția pe care doriți să o rulați" + }, + "SELECT_GROUP": { + "TITLE": "Setează Grupul", + "DESCRIPTION": "Dacă doriți să rulați numai pe un grup de evenimente, setați grupul aici" + }, + "SELECT_EVENT": { + "TITLE": "Selectați Evenimentul", + "DESCRIPTION": "Dacă doriți să rulați numai la un eveniment specific, specificați-l aici" + } + }, + "TARGET": { + "DESCRIPTION": "Puteți alege să rulați o țintă sau să o rulați în aceleași condiții ca și alte ținte.", + "TARGET": { + "DESCRIPTION": "Ținta pe care doriți să o rulați pentru această acțiune" + }, + "CONDITIONS": { + "DESCRIPTION": "Condiții de Execuție" + } + } + }, + "TABLE": { + "CONDITION": "Condiție", + "TYPE": "Tip", + "TARGET": "Țintă", + "CREATIONDATE": "Data Creării" + } + }, + "TARGET": { + "TITLE": "Ținte", + "DESCRIPTION": "O țintă este destinația codului pe care doriți să-l rulați dintr-o acțiune. Creați o țintă aici și adăugați-o la acțiunile dvs.", + "CREATE": { + "TITLE": "Creează-ți Ținta", + "DESCRIPTION": "Creați-vă propria țintă în afara Zitadel", + "NAME": "Nume", + "NAME_DESCRIPTION": "Dați țintei dvs. un nume clar, descriptiv, pentru a o identifica ușor mai târziu", + "TYPE": "Tip", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "Apel REST", + "restAsync": "REST Asincron" + }, + "ENDPOINT": "Punct Final", + "ENDPOINT_DESCRIPTION": "Introduceți punctul final unde este găzduit codul dvs. Asigurați-vă că este accesibil pentru noi!", + "TIMEOUT": "Timeout", + "TIMEOUT_DESCRIPTION": "Setați timpul maxim pe care ținta dvs. îl are pentru a răspunde. Dacă durează mai mult, vom opri cererea.", + "INTERRUPT_ON_ERROR": "Întrerupe la Eroare", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Opriți toate execuțiile când țintele returnează o eroare", + "INTERRUPT_ON_ERROR_WARNING": "Atenție: „Întrerupe la eroare” oprește operațiunile în caz de eșec, riscând blocarea accesului. Testați cu această opțiune dezactivată pentru a evita blocarea autentificării/creării.", + "AWAIT_RESPONSE": "Așteaptă Răspuns", + "AWAIT_RESPONSE_DESCRIPTION": "Vom aștepta un răspuns înainte de a face altceva. Util dacă intenționați să utilizați mai multe ținte pentru o singură acțiune" + }, + "TABLE": { + "NAME": "Nume", + "ENDPOINT": "Punct Final", + "CREATIONDATE": "Data Creării", + "REORDER": "Reordonați" + } + } + }, + "MEMBERROLES": { + "IAM_OWNER": "Are control asupra întregii instanțe, inclusiv asupra tuturor organizațiilor", + "IAM_OWNER_VIEWER": "Are permisiunea de a revizui întreaga instanță, inclusiv toate organizațiile", + "IAM_ORG_MANAGER": "Are permisiunea de a crea și gestiona organizații", + "IAM_USER_MANAGER": "Are permisiunea de a crea și gestiona utilizatori", + "IAM_ADMIN_IMPERSONATOR": "Are permisiunea de a impersona administratorul și utilizatorii finali din toate organizațiile", + "IAM_END_USER_IMPERSONATOR": "Are permisiunea de a impersona utilizatorii finali din toate organizațiile", + "IAM_LOGIN_CLIENT": "Are permisiunea de a gestiona clientii de login", + "ORG_OWNER": "Are permisiunea asupra întregii organizații", + "ORG_USER_MANAGER": "Are permisiunea de a crea și gestiona utilizatorii organizației", + "ORG_OWNER_VIEWER": "Are permisiunea de a revizui întreaga organizație", + "ORG_USER_PERMISSION_EDITOR": "Are permisiunea de a gestiona granturile utilizatorilor", + "ORG_PROJECT_PERMISSION_EDITOR": "Are permisiunea de a gestiona granturile proiectelor", + "ORG_PROJECT_CREATOR": "Are permisiunea de a-și crea propriile proiecte și setările de bază", + "ORG_ADMIN_IMPERSONATOR": "Are permisiunea de a impersona administratorul și utilizatorii finali din organizație", + "ORG_END_USER_IMPERSONATOR": "Are permisiunea de a impersona utilizatorii finali din organizație", + "PROJECT_OWNER": "Are permisiunea asupra întregului proiect", + "PROJECT_OWNER_VIEWER": "Are permisiunea de a revizui întregul proiect", + "PROJECT_OWNER_GLOBAL": "Are permisiunea asupra întregului proiect", + "PROJECT_OWNER_VIEWER_GLOBAL": "Are permisiunea de a revizui întregul proiect", + "PROJECT_GRANT_OWNER": "Are permisiunea de a gestiona grantul proiectului", + "PROJECT_GRANT_OWNER_VIEWER": "Are permisiunea de a revizui grantul proiectului" + }, + "OVERLAYS": { + "ORGSWITCHER": { + "TEXT": "Toate setările organizației și tabelele din consolă se bazează pe o organizație selectată. Faceți clic pe acest buton pentru a schimba organizația sau pentru a crea una nouă." + }, + "INSTANCE": { + "TEXT": "Faceți clic aici pentru a ajunge la setările implicite. Rețineți că aveți acces la acest buton numai dacă aveți permisiuni îmbunătățite." + }, + "PROFILE": { + "TEXT": "Aici puteți comuta între conturile dvs. de utilizator și vă puteți gestiona sesiunile și profilul." + }, + "NAV": { + "TEXT": "Această navigare se modifică în funcție de organizația selectată de mai sus sau de instanța dvs." + }, + "CONTEXTCHANGED": { + "TEXT": "Contextul organizației s-a modificat." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "Vizualizarea tocmai s-a schimbat în instanță!" + } + }, + "FILTER": { + "TITLE": "Filtru", + "STATE": "Stare", + "DISPLAYNAME": "Numele afișat al utilizatorului", + "EMAIL": "E-mail", + "USERNAME": "Numele utilizatorului", + "ORGNAME": "Numele organizației", + "PRIMARYDOMAIN": "Domeniu principal", + "PROJECTNAME": "Numele proiectului", + "RESOURCEOWNER": "Proprietarul resursei", + "METHODS": { + "5": "conține", + "7": "se termină cu", + "1": "este egal cu" + } + }, + "KEYBOARDSHORTCUTS": { + "TITLE": "Comenzi rapide de la tastatură", + "UNDERORGCONTEXT": "În cadrul paginilor organizației", + "SIDEWIDE": "Comenzi rapide la nivelul site-ului", + "SHORTCUTS": { + "HOME": "Du-te la Acasă", + "INSTANCE": "Du-te la Instanță", + "ORG": "Du-te la Organizație", + "ORGSETTINGS": "Du-te la Setările organizației", + "ORGSWITCHER": "Schimbă organizația", + "ME": "Du-te la propriul profil", + "PROJECTS": "Du-te la Proiecte", + "USERS": "Du-te la Utilizatori", + "USERGRANTS": "Du-te la Autorizații", + "ACTIONS": "Du-te la Acțiuni și Fluxuri", + "DOMAINS": "Du-te la Domenii" + } + }, + "RESOURCEID": "ID-ul resursei", + "NAME": "Nume", + "VERSION": "Versiune", + "TABLE": { + "NOROWS": "Fără date" + }, + "ERRORS": { + "REQUIRED": "Vă rugăm să completați acest câmp.", + "ATLEASTONE": "Furnizați cel puțin o valoare.", + "TOKENINVALID": { + "TITLE": "Tokenul dvs. de autorizare a expirat.", + "DESCRIPTION": "Faceți clic pe butonul de mai jos pentru a vă conecta din nou." + }, + "EXHAUSTED": { + "TITLE": "Instanța dvs. este blocată.", + "DESCRIPTION": "Solicitați administratorului instanței ZITADEL să actualizeze abonamentul." + }, + "INVALID_FORMAT": "Formatarea este nevalidă.", + "NOTANEMAIL": "Valoarea furnizată nu este o adresă de e-mail.", + "MINLENGTH": "Trebuie să aibă cel puțin {{requiredLength}} caractere.", + "MAXLENGTH": "Trebuie să aibă mai puțin de {{requiredLength}} caractere.", + "UPPERCASEMISSING": "Trebuie să includă o literă majusculă.", + "LOWERCASEMISSING": "Trebuie să includă o literă minusculă.", + "SYMBOLERROR": "Trebuie să includă un simbol sau un semn de punctuație.", + "NUMBERERROR": "Trebuie să includă o cifră.", + "PWNOTEQUAL": "Parolele furnizate nu se potrivesc.", + "PHONE": "Numărul de telefon trebuie să înceapă cu +." + }, + "USER": { + "SETTINGS": { + "TITLE": "Setări", + "GENERAL": "General", + "IDP": "Furnizori de identitate", + "SECURITY": "Parolă și securitate", + "KEYS": "Chei", + "PAT": "Tokenuri de acces personal", + "USERGRANTS": "Autorizații", + "MEMBERSHIPS": "Afilieri", + "METADATA": "Metadata" + }, + "TITLE": "Informații personale", + "DESCRIPTION": "Gestionați-vă informațiile și setările de securitate.", + "PAGES": { + "TITLE": "Utilizator", + "DETAIL": "Detalii", + "CREATE": "Creare", + "MY": "Informațiile mele", + "LOGINNAMES": "Nume de conectare", + "LOGINMETHODS": "Metode de conectare", + "LOGINNAMESDESC": "Acestea sunt numele dvs. de conectare:", + "NOUSER": "Niciun utilizator asociat.", + "REACTIVATE": "Reactivați", + "DEACTIVATE": "Dezactivați", + "FILTER": "Filtru", + "STATE": "Stare", + "DELETE": "Ștergeți utilizatorul", + "UNLOCK": "Deblocați utilizatorul", + "GENERATESECRET": "Generați secretul clientului", + "REMOVESECRET": "Eliminați secretul clientului", + "LOCKEDDESCRIPTION": "Acest utilizator a fost blocat din cauza depășirii numărului maxim de încercări de conectare și trebuie deblocat pentru a fi utilizat din nou.", + "DELETEACCOUNT": "Ștergeți contul", + "DELETEACCOUNT_DESC": "Dacă efectuați această acțiune, veți fi deconectat și nu veți mai avea acces la contul dvs. Această acțiune nu este reversibilă, așa că vă rugăm să continuați cu prudență.", + "DELETEACCOUNT_BTN": "Ștergeți contul", + "DELETEACCOUNT_SUCCESS": "Cont șters cu succes!" + }, + "DETAILS": { + "DATECREATED": "Creat", + "DATECHANGED": "Modificat" + }, + "DIALOG": { + "DELETE_TITLE": "Ștergeți utilizatorul", + "DELETE_SELF_TITLE": "Ștergeți contul", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți definitiv un utilizator. Sigur doriți să continuați?", + "DELETE_SELF_DESCRIPTION": "Sunteți pe cale să ștergeți definitiv contul dvs. personal. Aceasta vă va deconecta și vă va șterge utilizatorul. Această acțiune nu poate fi anulată!", + "DELETE_AUTH_DESCRIPTION": "Sunteți pe cale să ștergeți definitiv contul dvs. personal. Sigur doriți să continuați?", + "TYPEUSERNAME": "Tastați '{{value}}' pentru a confirma și a șterge utilizatorul.", + "USERNAME": "Nume de conectare", + "DELETE_BTN": "Ștergeți definitiv" + }, + "SENDEMAILDIALOG": { + "TITLE": "Trimiteți notificarea prin e-mail", + "DESCRIPTION": "Faceți clic pe butonul de mai jos pentru a trimite o notificare la adresa de e-mail curentă sau modificați adresa de e-mail din câmp.", + "NEWEMAIL": "Adresă de e-mail nouă" + }, + "SECRETDIALOG": { + "CLIENTSECRET": "Secretul clientului", + "CLIENTSECRET_DESCRIPTION": "Păstrați secretul clientului într-un loc sigur, deoarece va dispărea odată ce dialogul este închis." + }, + "TABLE": { + "DEACTIVATE": "Dezactivați", + "ACTIVATE": "Activați", + "CHANGEDATE": "Ultima modificare", + "CREATIONDATE": "Creat la", + "FILTER": { + "0": "Filtrați după Numele afișat", + "1": "Filtrați după Numele de utilizator", + "2": "filtrați după Numele afișat", + "3": "filtrați după Numele de utilizator", + "4": "filtrați după E-mail", + "5": "filtrați după Numele afișat", + "10": "filtrați după numele organizației", + "12": "filtrați după numele proiectului" + }, + "EMPTY": "Nicio înregistrare" + }, + "PASSWORDLESS": { + "SEND": "Trimiteți linkul de înregistrare", + "TABLETYPE": "Tip", + "TABLESTATE": "Stare", + "NAME": "Nume", + "EMPTY": "Niciun dispozitiv setat", + "TITLE": "Autentificare fără parolă", + "DESCRIPTION": "Adăugați metode de autentificare bazate pe WebAuthn pentru a vă conecta la ZITADEL fără parolă.", + "MANAGE_DESCRIPTION": "Gestionați metodele de al doilea factor ale utilizatorilor dvs.", + "U2F": "Adăugați metodă", + "U2F_DIALOG_TITLE": "Verificați autentificatorul", + "U2F_DIALOG_DESCRIPTION": "Introduceți un nume pentru conectarea fără parolă utilizată", + "U2F_SUCCESS": "Autentificare fără parolă creată cu succes!", + "U2F_ERROR": "A apărut o eroare în timpul configurării!", + "U2F_NAME": "Numele autentificatorului", + "TYPE": { + "0": "Niciun MFA definit", + "1": "Parolă unică (OTP)", + "2": "Amprentă, chei de securitate, Face ID și altele" + }, + "STATE": { + "0": "Nicio stare", + "1": "Nu este gata", + "2": "Gata", + "3": "Șters" + }, + "DIALOG": { + "DELETE_TITLE": "Eliminați metoda de autentificare fără parolă", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți o metodă de autentificare fără parolă. Sigur doriți să continuați?", + "ADD_TITLE": "Autentificare fără parolă", + "ADD_DESCRIPTION": "Selectați una dintre opțiunile disponibile pentru a crea o metodă de autentificare fără parolă.", + "SEND_DESCRIPTION": "Trimiteți-vă un link de înregistrare la adresa dvs. de e-mail.", + "SEND": "Trimiteți linkul de înregistrare", + "SENT": "E-mailul a fost livrat cu succes. Verificați-vă căsuța poștală pentru a continua cu configurarea.", + "QRCODE_DESCRIPTION": "Generați codul QR pentru scanare cu un alt dispozitiv.", + "QRCODE": "Generați codul QR", + "QRCODE_SCAN": "Scanați acest cod QR pentru a continua cu configurarea pe dispozitivul dvs.", + "NEW_DESCRIPTION": "Utilizați acest dispozitiv pentru a configura Autentificarea fără parolă.", + "NEW": "Adăugați nou" + } + }, + "MFA": { + "TABLETYPE": "Tip", + "TABLESTATE": "Stare", + "NAME": "Nume", + "EMPTY": "Niciun factor suplimentar", + "TITLE": "Autentificare multifactor", + "DESCRIPTION": "Adăugați un al doilea factor pentru a asigura o securitate optimă pentru contul dvs.", + "MANAGE_DESCRIPTION": "Gestionați metodele de al doilea factor ale utilizatorilor dvs.", + "ADD": "Adăugați factor", + "OTP": "Aplicație de autentificare pentru TOTP (Parolă unică bazată pe timp)", + "OTP_DIALOG_TITLE": "Adăugați OTP", + "OTP_DIALOG_DESCRIPTION": "Scanați codul QR cu o aplicație de autentificare și introduceți codul de mai jos pentru a verifica și a activa metoda OTP.", + "U2F": "Amprentă, chei de securitate, Face ID și altele", + "U2F_DIALOG_TITLE": "Verificați factorul", + "U2F_DIALOG_DESCRIPTION": "Introduceți un nume pentru multifactorul universal utilizat.", + "U2F_SUCCESS": "Factor adăugat cu succes!", + "U2F_ERROR": "A apărut o eroare în timpul configurării!", + "U2F_NAME": "Numele autentificatorului", + "OTPSMS": "OTP (Parolă unică) cu SMS", + "OTPEMAIL": "OTP (Parolă unică) cu e-mail", + "SETUPOTPSMSDESCRIPTION": "Doriți să configurați acest număr de telefon ca al doilea factor OTP (parolă unică)?", + "OTPSMSSUCCESS": "Factorul OTP configurat cu succes.", + "OTPSMSPHONEMUSTBEVERIFIED": "Telefonul dvs. trebuie să fie verificat pentru a utiliza această metodă.", + "OTPEMAILSUCCESS": "Factorul OTP configurat cu succes.", + "TYPE": { + "0": "Niciun MFA definit", + "1": "Parolă unică (OTP)", + "2": "Amprentă, chei de securitate, Face ID și altele" + }, + "STATE": { + "0": "Nicio stare", + "1": "Nu este gata", + "2": "Gata", + "3": "Șters" + }, + "DIALOG": { + "MFA_DELETE_TITLE": "Eliminați al doilea factor", + "MFA_DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un al doilea factor. Sigur doriți să continuați?", + "ADD_MFA_TITLE": "Adăugați al doilea factor", + "ADD_MFA_DESCRIPTION": "Selectați una dintre următoarele opțiuni." + } + }, + "EXTERNALIDP": { + "TITLE": "Furnizori de identitate externi", + "DESC": "", + "IDPCONFIGID": "ID-ul config-ului IDP", + "IDPNAME": "Numele IDP", + "USERDISPLAYNAME": "Nume extern", + "EXTERNALUSERID": "ID-ul utilizatorului extern", + "EMPTY": "Niciun IdP extern găsit", + "DIALOG": { + "DELETE_TITLE": "Eliminați IdP", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un furnizor de identitate de la un utilizator. Doriți cu adevărat să continuați?" + } + }, + "CREATE": { + "TITLE": "Creați un utilizator nou", + "DESCRIPTION": "Vă rugăm să furnizați informațiile necesare.", + "NAMEANDEMAILSECTION": "Nume și e-mail", + "GENDERLANGSECTION": "Sex și limbă", + "PHONESECTION": "Numere de telefon", + "PASSWORDSECTION": "Parola inițială", + "ADDRESSANDPHONESECTION": "Număr de telefon", + "INITMAILDESCRIPTION": "Dacă ambele opțiuni sunt selectate, nu va fi trimis niciun e-mail pentru inițializare. Dacă este selectată doar una dintre opțiuni, va fi trimis un e-mail pentru a furniza / verifica datele.", + "SETUPAUTHENTICATIONLATER": "Configurați autentificarea mai târziu pentru acest utilizator.", + "INVITATION": "Trimiteți un e-mail de invitație pentru configurarea autentificării și verificarea e-mailului.", + "INITIALPASSWORD": "Setați o parolă inițială pentru utilizator." + }, + "CODEDIALOG": { + "TITLE": "Verificați numărul de telefon", + "DESCRIPTION": "Introduceți codul pe care l-ați primit prin mesaj text pentru a vă verifica numărul de telefon.", + "CODE": "Cod" + }, + "DATA": { + "STATE": "Stare", + "STATE0": "Necunoscut", + "STATE1": "Activ", + "STATE2": "Inactiv", + "STATE3": "Șters", + "STATE4": "Blocat", + "STATE5": "Suspendat", + "STATE6": "Inițial" + }, + "PROFILE": { + "TITLE": "Profil", + "EMAIL": "E-mail", + "PHONE": "Număr de telefon", + "PHONE_HINT": "Utilizați simbolul + urmat de codul de țară, sau selectați țara din lista derulantă și introduceți în cele din urmă numărul de telefon", + "PHONE_VERIFIED": "Numărul de telefon verificat", + "SEND_SMS": "Trimiteți SMS de verificare", + "SEND_EMAIL": "Trimiteți E-mail", + "USERNAME": "Nume de utilizator", + "CHANGEUSERNAME": "modificați", + "CHANGEUSERNAME_TITLE": "Modificați numele de utilizator", + "CHANGEUSERNAME_DESC": "Introduceți noul nume în câmpul de mai jos.", + "FIRSTNAME": "Prenume", + "LASTNAME": "Nume de familie", + "NICKNAME": "Poreclă", + "DISPLAYNAME": "Numele afișat", + "PREFERREDLOGINNAME": "Numele de conectare preferat", + "PREFERRED_LANGUAGE": "Limbă", + "GENDER": "Sex", + "PASSWORD": "Parolă", + "AVATAR": { + "UPLOADTITLE": "Încărcați-vă poza de profil", + "UPLOADBTN": "Alegeți fișier", + "UPLOAD": "Încărcați", + "CURRENT": "Poza actuală", + "PREVIEW": "Previzualizare", + "DELETESUCCESS": "Șters cu succes!", + "CROPPERERROR": "A apărut o eroare în timpul încărcării fișierului dvs. Încercați un format și o dimensiune diferite, dacă este necesar." + }, + "COUNTRY": "Țară" + }, + "MACHINE": { + "TITLE": "Detalii utilizator de serviciu", + "USERNAME": "Nume de utilizator", + "NAME": "Nume", + "DESCRIPTION": "Descriere", + "KEYSTITLE": "Chei", + "KEYSDESC": "Definiți-vă cheile și adăugați o dată de expirare opțională.", + "TOKENSTITLE": "Tokenuri de acces personal", + "TOKENSDESC": "Tokenurile de acces personal funcționează ca tokenurile de acces OAuth obișnuite.", + "ID": "ID cheie", + "TYPE": "Tip", + "EXPIRATIONDATE": "Data de expirare", + "CHOOSEDATEAFTER": "Introduceți o expirare validă după", + "CHOOSEEXPIRY": "Selectați o dată de expirare", + "CREATIONDATE": "Data creării", + "KEYDETAILS": "Detalii cheie", + "ACCESSTOKENTYPE": "Tipul tokenului de acces", + "ACCESSTOKENTYPES": { + "0": "Bearer", + "1": "JWT" + }, + "ADD": { + "TITLE": "Adăugați cheie", + "DESCRIPTION": "Selectați tipul cheii și alegeți o dată de expirare opțională." + }, + "ADDED": { + "TITLE": "Cheia a fost creată", + "DESCRIPTION": "Descărcați cheia, deoarece nu va mai fi vizibilă după închiderea acestui dialog!" + }, + "KEYTYPES": { + "1": "JSON" + }, + "DIALOG": { + "DELETE_KEY": { + "TITLE": "Ștergeți cheia", + "DESCRIPTION": "Doriți să ștergeți cheia selectată? Această acțiune nu poate fi anulată." + } + } + }, + "PASSWORD": { + "TITLE": "Parolă", + "LABEL": "O parolă sigură ajută la protejarea contului", + "DESCRIPTION": "Introduceți noua parolă conform politicii de mai jos.", + "OLD": "Parola curentă", + "NEW": "Parolă nouă", + "CONFIRM": "Confirmați parola nouă", + "NEWINITIAL": "Parolă", + "CONFIRMINITIAL": "Confirmați parola", + "RESET": "Resetați parola curentă", + "SET": "Setați parola nouă", + "RESENDNOTIFICATION": "Trimiteți linkul de resetare a parolei", + "REQUIRED": "Lipsesc unele câmpuri obligatorii.", + "MINLENGTHERROR": "Trebuie să aibă cel puțin {{value}} caractere.", + "MAXLENGTHERROR": "Trebuie să aibă mai puțin de {{value}} caractere." + }, + "ID": "ID", + "EMAIL": "E-mail", + "PHONE": "Număr de telefon", + "PHONEEMPTY": "Niciun număr de telefon definit", + "PHONEVERIFIED": "Numărul de telefon verificat.", + "EMAILVERIFIED": "E-mail verificat", + "NOTVERIFIED": "neverificat", + "PREFERRED_LOGINNAME": "Nume de conectare preferat", + "ISINITIAL": "Utilizatorul nu este încă activ.", + "LOGINMETHODS": { + "TITLE": "Informații de contact", + "DESCRIPTION": "Informațiile furnizate sunt utilizate pentru a vă trimite informații importante, cum ar fi e-mailurile de resetare a parolei.", + "EMAIL": { + "TITLE": "E-mail", + "VALID": "validat", + "ISVERIFIED": "E-mail verificat", + "ISVERIFIEDDESC": "Dacă e-mailul este indicat ca verificat, nu va fi făcută nicio solicitare de verificare a e-mailului.", + "RESEND": "Retrimiteți e-mailul de verificare", + "EDITTITLE": "Modificați e-mailul", + "EDITDESC": "Introduceți noul e-mail în câmpul de mai jos." + }, + "PHONE": { + "TITLE": "Telefon", + "VALID": "validat", + "RESEND": "Retrimiteți mesajul text de verificare", + "EDITTITLE": "Modificați numărul", + "EDITVALUE": "Număr de telefon", + "EDITDESC": "Introduceți noul număr de telefon în câmpul de mai jos.", + "DELETETITLE": "Ștergeți numărul de telefon", + "DELETEDESC": "Doriți cu adevărat să ștergeți numărul de telefon", + "OTPSMSREMOVALWARNING": "Acest cont utilizează acest număr de telefon ca al doilea factor. Nu veți putea să îl utilizați după ce continuați." + }, + "RESENDCODE": "Retrimiteți codul", + "ENTERCODE": "Verificați", + "ENTERCODE_DESC": "Verificați codul" + }, + "GRANTS": { + "TITLE": "Granturi de utilizator", + "DESCRIPTION": "Acordați acestui utilizator acces la anumite proiecte", + "CREATE": { + "TITLE": "Creați grant de utilizator", + "DESCRIPTION": "Căutați organizația, proiectul și rolurile de proiect corespunzătoare." + }, + "PROJECTNAME": "Numele proiectului", + "PROJECT-OWNED": "Proiect", + "PROJECT-GRANTED": "Proiect acordat", + "FILTER": { + "0": "filtrați după utilizator", + "1": "filtrați după domeniu", + "2": "filtrați după numele proiectului", + "3": "filtrați după numele rolului" + } + }, + "STATE": { + "0": "Necunoscut", + "1": "Activ", + "2": "Inactiv", + "3": "Șters", + "4": "Blocat", + "5": "Suspendat", + "6": "Inițial" + }, + "STATEV2": { + "0": "Necunoscut", + "1": "Activ", + "2": "Inactiv", + "3": "Șters", + "4": "Blocat", + "5": "Inițial" + }, + "SEARCH": { + "ADDITIONAL": "Nume de conectare (organizația curentă)", + "ADDITIONAL-EXTERNAL": "Nume de conectare (organizație externă)" + }, + "TARGET": { + "SELF": "Dacă doriți să acordați un utilizator al unei alte organizații", + "EXTERNAL": "Pentru a acorda un utilizator al organizației dvs.", + "CLICKHERE": "faceți clic aici" + }, + "SIGNEDOUT": "Sunteți deconectat. Faceți clic pe butonul \"Conectare\" pentru a vă conecta din nou.", + "SIGNEDOUT_BTN": "Conectare", + "EDITACCOUNT": "Editați contul", + "ADDACCOUNT": "Conectați-vă cu un alt cont", + "RESENDINITIALEMAIL": "Retrimiteți e-mailul de activare", + "RESENDEMAILNOTIFICATION": "Retrimiteți notificarea prin e-mail", + "TOAST": { + "CREATED": "Utilizator creat cu succes.", + "SAVED": "Profil salvat cu succes.", + "USERNAMECHANGED": "Nume de utilizator modificat.", + "EMAILSAVED": "E-mail salvat cu succes.", + "INITEMAILSENT": "E-mail de inițializare trimis.", + "PHONESAVED": "Telefon salvat cu succes.", + "PHONEREMOVED": "Telefon eliminat.", + "PHONEVERIFIED": "Telefon verificat cu succes.", + "PHONEVERIFICATIONSENT": "Codul de verificare a telefonului trimis.", + "EMAILVERIFICATIONSENT": "Codul de verificare a e-mailului trimis.", + "OTPREMOVED": "OTP eliminat.", + "U2FREMOVED": "Factor eliminat.", + "PASSWORDLESSREMOVED": "Fără parolă eliminat.", + "INITIALPASSWORDSET": "Parola inițială setată.", + "PASSWORDNOTIFICATIONSENT": "Notificarea de modificare a parolei trimisă.", + "PASSWORDCHANGED": "Parola modificată cu succes.", + "REACTIVATED": "Utilizatorul reactivat.", + "DEACTIVATED": "Utilizatorul dezactivat.", + "SELECTEDREACTIVATED": "Utilizatorii selectați au fost reactivați.", + "SELECTEDDEACTIVATED": "Utilizatorii selectați au fost dezactivați.", + "SELECTEDKEYSDELETED": "Cheile selectate au fost șterse.", + "KEYADDED": "Cheie adăugată!", + "MACHINEADDED": "Utilizator de serviciu creat!", + "DELETED": "Utilizator șters cu succes!", + "UNLOCKED": "Utilizator deblocat cu succes!", + "PASSWORDLESSREGISTRATIONSENT": "Link de înregistrare trimis cu succes.", + "SECRETGENERATED": "Secret generat cu succes!", + "SECRETREMOVED": "Secret eliminat cu succes!" + }, + "MEMBERSHIPS": { + "TITLE": "Roluri de manager ZITADEL", + "DESCRIPTION": "Acestea sunt toate granturile de membru ale utilizatorului. Puteți să le modificați și în paginile de detalii ale organizației, proiectului sau IAM.", + "ORGCONTEXT": "Vedeți toate organizațiile și proiectele care sunt legate de organizația selectată în prezent.", + "USERCONTEXT": "Vedeți toate organizațiile și proiectele la care sunteți autorizat. Inclusiv alte organizații.", + "CREATIONDATE": "Data creării", + "CHANGEDATE": "Ultima modificare", + "DISPLAYNAME": "Numele afișat", + "REMOVE": "Eliminați", + "TYPE": "Tip", + "ORGID": "ID-ul organizației", + "UPDATED": "Afilierea a fost actualizată.", + "NOPERMISSIONTOEDIT": "Vă lipsesc permisiunile necesare pentru a edita rolurile!", + "TYPES": { + "UNKNOWN": "Necunoscut", + "ORG": "Organizație", + "PROJECT": "Proiect", + "GRANTEDPROJECT": "Proiect acordat" + } + }, + "PERSONALACCESSTOKEN": { + "ID": "ID", + "TOKEN": "Token", + "ADD": { + "TITLE": "Generați un token de acces personal nou", + "DESCRIPTION": "Definiți o expirare personalizată pentru token.", + "CHOOSEEXPIRY": "Selectați o dată de expirare", + "CHOOSEDATEAFTER": "Introduceți o expirare validă după" + }, + "ADDED": { + "TITLE": "Token de acces personal", + "DESCRIPTION": "Asigurați-vă că vă copiați tokenul de acces personal. Nu veți mai putea să îl vedeți din nou!" + }, + "DELETE": { + "TITLE": "Ștergeți tokenul", + "DESCRIPTION": "Sunteți pe cale să ștergeți tokenul de acces personal. Sigur doriți să continuați?" + }, + "DELETED": "Token șters cu succes." + } + }, + "METADATA": { + "TITLE": "Metadata", + "KEY": "Cheie", + "VALUE": "Valoare", + "ADD": "Înregistrare nouă", + "SAVE": "Salvați", + "EMPTY": "Nicio metadată", + "SETSUCCESS": "Element salvat cu succes", + "REMOVESUCCESS": "Element șters cu succes" + }, + "FLOWS": { + "ID": "ID", + "NAME": "Nume", + "STATE": "Stare", + "STATES": { + "0": "nicio stare", + "1": "inactiv", + "2": "activ" + }, + "ADDTRIGGER": "Adăugați declanșator", + "FLOWCHANGED": "Fluxul a fost modificat cu succes", + "FLOWCLEARED": "Fluxul a fost resetat cu succes", + "TIMEOUT": "Timeout", + "TIMEOUTINSEC": "Timeout în secunde", + "ALLOWEDTOFAIL": "Permis să eșueze", + "ALLOWEDTOFAILWARN": { + "TITLE": "Avertisment", + "DESCRIPTION": "Dacă dezactivați această setare, este posibil ca utilizatorii din organizația dvs. să nu se poată conecta. În plus, nu veți mai putea accesa consola pentru a dezactiva acțiunea. Vă recomandăm să creați un utilizator administrator într-o organizație separată sau să testați mai întâi scripturile într-un mediu de dezvoltare sau într-o organizație de dezvoltare." + }, + "SCRIPT": "Script", + "FLOWTYPE": "Tip flux", + "TRIGGERTYPE": "Tip declanșator", + "ACTIONS": "Acțiuni", + "ACTIONSMAX": "Pe baza nivelului dvs., aveți la dispoziție un număr limitat de acțiuni ({{value}}). Asigurați-vă că dezactivați pe cele de care nu aveți nevoie sau luați în considerare actualizarea nivelului.", + "DIALOG": { + "ADD": { + "TITLE": "Creați o acțiune" + }, + "UPDATE": { + "TITLE": "Actualizați acțiunea" + }, + "DELETEACTION": { + "TITLE": "Ștergeți acțiunea?", + "DESCRIPTION": "Sunteți pe cale să ștergeți o acțiune. Aceasta nu poate fi inversată. Sigur doriți să continuați?", + "DELETE_SUCCESS": "Acțiunea a fost ștearsă cu succes." + }, + "CLEAR": { + "TITLE": "Ștergeți fluxul?", + "DESCRIPTION": "Sunteți pe cale să resetați fluxul împreună cu declanșatoarele și acțiunile sale. Această modificare nu poate fi restabilită. Sigur doriți să continuați?" + }, + "REMOVEACTIONSLIST": { + "TITLE": "Ștergeți acțiunile selectate?", + "DESCRIPTION": "Sigur doriți să ștergeți acțiunile selectate din flux?" + }, + "ABOUTNAME": "Numele acțiunii și numele funcției din javascript trebuie să fie aceleași" + }, + "TOAST": { + "ACTIONSSET": "Acțiuni setate", + "ACTIONREACTIVATED": "Acțiuni reactivate cu succes", + "ACTIONDEACTIVATED": "Acțiuni dezactivate cu succes" + } + }, + "IAM": { + "POLICIES": { + "TITLE": "Politici de sistem și setări de acces", + "DESCRIPTION": "Gestionați-vă politicile globale și setările de acces pentru gestionare." + }, + "EVENTSTORE": { + "TITLE": "Administrarea stocării IAM", + "DESCRIPTION": "Gestionați-vă vizualizările ZITADEL și evenimentele eșuate." + }, + "MEMBER": { + "TITLE": "Manageri", + "DESCRIPTION": "Acești manageri au permisiunea de a face modificări în instanța dvs." + }, + "PAGES": { + "STATE": "Stare", + "DOMAINLIST": "Domenii personalizate" + }, + "STATE": { + "0": "Nespecificat", + "1": "Creare", + "2": "Rulează", + "3": "Oprire", + "4": "Oprit" + }, + "VIEWS": { + "VIEWNAME": "Nume", + "DATABASE": "Bază de date", + "SEQUENCE": "Secvență", + "EVENTTIMESTAMP": "Marcaj de timp", + "LASTSPOOL": "Spool cu succes", + "ACTIONS": "Acțiuni", + "CLEAR": "Ștergeți", + "CLEARED": "Vizualizarea a fost ștearsă cu succes!", + "DIALOG": { + "VIEW_CLEAR_TITLE": "Ștergeți vizualizarea", + "VIEW_CLEAR_DESCRIPTION": "Sunteți pe cale să ștergeți o vizualizare. Ștergerea unei vizualizări creează un proces în timpul căruia este posibil ca datele să nu fie disponibile pentru utilizatorii finali. Sigur doriți să continuați?" + } + }, + "FAILEDEVENTS": { + "VIEWNAME": "Nume", + "DATABASE": "Bază de date", + "FAILEDSEQUENCE": "Secvență eșuată", + "FAILURECOUNT": "Număr de eșecuri", + "LASTFAILED": "Ultima eroare la", + "ERRORMESSAGE": "Mesaj de eroare", + "ACTIONS": "Acțiuni", + "DELETE": "Eliminați", + "DELETESUCCESS": "Evenimente eșuate eliminate." + }, + "EVENTS": { + "EDITOR": "Editor", + "EDITORID": "ID editor", + "AGGREGATE": "Agregat", + "AGGREGATEID": "ID agregat", + "AGGREGATETYPE": "Tip agregat", + "RESOURCEOWNER": "Proprietarul resursei", + "SEQUENCE": "Secvență", + "CREATIONDATE": "Creat la", + "TYPE": "Tip", + "PAYLOAD": "Sarcină utilă", + "FILTERS": { + "BTN": "Filtru", + "USER": { + "IDLABEL": "ID", + "CHECKBOX": "Filtrați după editor" + }, + "AGGREGATE": { + "TYPELABEL": "Tip agregat", + "IDLABEL": "ID", + "CHECKBOX": "Filtrați după agregat" + }, + "TYPE": { + "TYPELABEL": "Tip", + "CHECKBOX": "Filtrați după tip" + }, + "RESOURCEOWNER": { + "LABEL": "ID", + "CHECKBOX": "Filtrați după proprietarul resursei" + }, + "SEQUENCE": { + "LABEL": "Secvență", + "CHECKBOX": "Filtrați după secvență" + }, + "SORT": "Sortați", + "ASC": "Ascendent", + "DESC": "Descendent", + "CREATIONDATE": { + "RADIO_FROM": "De la", + "RADIO_RANGE": "Interval", + "LABEL_SINCE": "De când", + "LABEL_UNTIL": "Până la" + }, + "OTHER": "altele", + "OTHERS": "altele" + }, + "DIALOG": { + "TITLE": "Detalii eveniment" + } + }, + "TOAST": { + "MEMBERREMOVED": "Manager eliminat.", + "MEMBERSADDED": "Manageri adăugați.", + "MEMBERADDED": "Manager adăugat.", + "MEMBERCHANGED": "Manager modificat.", + "ROLEREMOVED": "Rol eliminat.", + "ROLECHANGED": "Rol modificat.", + "REACTIVATED": "Reactivat", + "DEACTIVATED": "Dezactivat" + } + }, + "ORG": { + "PAGES": { + "NAME": "Nume", + "ID": "ID", + "CREATIONDATE": "Data creării", + "DATECHANGED": "Modificat", + "FILTER": "Filtru", + "FILTERPLACEHOLDER": "Filtrați după nume", + "LIST": "Organizații", + "LISTDESCRIPTION": "Alegeți o organizație.", + "ACTIVE": "Activ", + "CREATE": "Creați organizație", + "DEACTIVATE": "Dezactivați organizația", + "REACTIVATE": "Reactivați organizația", + "NOPERMISSION": "Nu aveți permisiunea de a accesa setările organizației.", + "USERSELFACCOUNT": "Utilizați-vă contul personal ca proprietar al organizației", + "ORGDETAIL_TITLE": "Introduceți numele și domeniul noii dvs. organizații.", + "ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Introduceți numele noii dvs. organizații.", + "ORGDETAILUSER_TITLE": "Configurați proprietarul organizației", + "DELETE": "Ștergeți organizația", + "DEFAULTLABEL": "Implicit", + "SETASDEFAULT": "Setați ca organizație implicită", + "DEFAULTORGSET": "Organizația implicită a fost modificată cu succes", + "RENAME": { + "ACTION": "Redenumiți", + "TITLE": "Redenumiți organizația", + "DESCRIPTION": "Introduceți noul nume pentru organizația dvs.", + "BTN": "Redenumiți" + }, + "ORGDOMAIN": { + "TITLE": "Verificați proprietatea {{value}}", + "VERIFICATION": "Vă oferim două metode pentru a vă valida manual domeniul:", + "VERIFICATION_HTML": "- HTTP. Găzduiți un fișier temporar de verificare pe site-ul dvs. web", + "VERIFICATION_DNS": "- DNS. Creați o înregistrare DNS TXT", + "VERIFICATION_DNS_DESC": "Dacă gestionați {{ value }} și aveți acces la înregistrările DNS, puteți crea o înregistrare TXT nouă cu următoarele valori:", + "VERIFICATION_DNS_HOST_LABEL": "Gazdă:", + "VERIFICATION_DNS_CHALLENGE_LABEL": "Utilizați acest cod pentru valoarea înregistrării TXT:", + "VERIFICATION_HTTP_DESC": "Dacă aveți acces la găzduirea site-ului dvs. web, pur și simplu descărcați fișierul de verificare și încărcați-l la adresa URL furnizată", + "VERIFICATION_HTTP_URL_LABEL": "Adresă URL așteptată:", + "VERIFICATION_HTTP_FILE_LABEL": "Fișier de verificare:", + "VERIFICATION_SKIP": "Puteți sări peste verificarea deocamdată și să continuați să vă creați organizația, dar pentru a utiliza domeniul, acest pas trebuie finalizat!", + "VERIFICATION_VALIDATION_DESC": "Nu ștergeți codul de verificare, deoarece ZITADEL va verifica din când în când proprietatea domeniului dvs.", + "VERIFICATION_NEWTOKEN_TITLE": "Solicitați un token nou", + "VERIFICATION_VALIDATION_ONGOING": "Metoda {{ value }} a fost selectată pentru a vă verifica domeniul. Faceți clic pe buton pentru a declanșa o verificare de verificare sau pentru a reseta procesul de verificare.", + "VERIFICATION_SUCCESSFUL": "Domeniu verificat cu succes!", + "RESETMETHOD": "Resetați metoda de verificare" + }, + "DOWNLOAD_FILE": "Descărcați fișierul", + "SELECTORGTOOLTIP": "Selectați această organizație.", + "PRIMARYDOMAIN": "Domeniu principal", + "STATE": "Stare", + "USEPASSWORD": "Setați parola inițială", + "USEPASSWORDDESC": "Utilizatorul nu trebuie să seteze parola în timpul inițializării." + }, + "LIST": { + "TITLE": "Organizații", + "DESCRIPTION": "Acestea sunt organizațiile din instanța dvs." + }, + "DOMAINS": { + "NEW": "Adăugați domeniu", + "TITLE": "Domenii verificate", + "DESCRIPTION": "Configurați domeniile organizației dvs. Acest domeniu poate fi utilizat pentru descoperirea domeniului și sufixarea numelui de utilizator.", + "SETPRIMARY": "Setați ca principal", + "DELETE": { + "TITLE": "Ștergeți domeniul", + "DESCRIPTION": "Sunteți pe cale să ștergeți unul dintre domeniile dvs." + }, + "ADD": { + "TITLE": "Adăugați domeniu", + "DESCRIPTION": "Sunteți pe cale să adăugați un domeniu pentru organizația dvs. După un proces de succes, domeniul poate fi utilizat pentru descoperirea domeniului și ca sufix pentru utilizatorii dvs." + } + }, + "STATE": { + "0": "Nedefinit", + "1": "Activ", + "2": "Dezactivat" + }, + "MEMBER": { + "TITLE": "Manageri de organizație", + "DESCRIPTION": "Definiți utilizatorii care vă pot modifica preferințele organizațiilor." + }, + "TOAST": { + "UPDATED": "Organizația a fost actualizată cu succes.", + "DEACTIVATED": "Organizația a fost dezactivată.", + "REACTIVATED": "Organizația a fost reactivată.", + "DOMAINADDED": "Domeniu adăugat.", + "DOMAINREMOVED": "Domeniu eliminat.", + "MEMBERADDED": "Manager adăugat.", + "MEMBERREMOVED": "Manager eliminat.", + "MEMBERCHANGED": "Manager modificat.", + "SETPRIMARY": "Domeniu principal setat.", + "DELETED": "Organizația a fost ștearsă cu succes", + "DEFAULTORGNOTFOUND": "Organizația implicită nu a fost găsită", + "ORG_WAS_DELETED": "Organizația a fost ștearsă." + }, + "DIALOG": { + "DEACTIVATE": { + "TITLE": "Dezactivați organizația", + "DESCRIPTION": "Sunteți pe cale să vă dezactivați organizația. Utilizatorii nu se vor mai putea conecta după aceea. Sigur doriți să continuați?" + }, + "REACTIVATE": { + "TITLE": "Reactivați organizația", + "DESCRIPTION": "Sunteți pe cale să vă reactivați organizația. Utilizatorii se vor putea conecta din nou. Sigur doriți să continuați?" + }, + "DELETE": { + "TITLE": "Ștergeți organizația", + "DESCRIPTION": "Sunteți pe cale să vă ștergeți organizația. Aceasta inițiază un proces în care vor fi șterse toate datele legate de organizație. Nu puteți anula această acțiune deocamdată.", + "TYPENAME": "Tastați '{{value}}' pentru a vă șterge organizația.", + "ORGNAME": "Nume", + "BTN": "Ștergeți" + } + } + }, + "SETTINGS": { + "LIST": { + "ORGS": "Organizații", + "FEATURESETTINGS": "Caracteristici", + "LANGUAGES": "Limbi", + "LOGIN": "Comportament și securitate la conectare", + "LOCKOUT": "Blocare", + "AGE": "Expirarea parolei", + "COMPLEXITY": "Complexitatea parolei", + "NOTIFICATIONS": "Notificări", + "SMTP_PROVIDER": "Furnizor SMTP", + "SMS_PROVIDER": "Furnizor SMS/Telefon", + "NOTIFICATIONS_DESC": "Setări SMTP și SMS", + "MESSAGETEXTS": "Texte de mesaje", + "IDP": "Furnizori de identitate", + "VERIFIED_DOMAINS": "Domenii verificate", + "DOMAIN": "Setări domeniu", + "LOGINTEXTS": "Texte interfață de conectare", + "BRANDING": "Branding", + "PRIVACYPOLICY": "Linkuri externe", + "OIDC": "Durata de viață și expirarea tokenului OIDC", + "WEB_KEYS": "OIDC Web Keys", + "SECRETS": "Generator de secrete", + "SECURITY": "Setări de securitate", + "EVENTS": "Evenimente", + "FAILEDEVENTS": "Evenimente eșuate", + "VIEWS": "Vizualizări" + }, + "GROUPS": { + "GENERAL": "Informații generale", + "NOTIFICATIONS": "Notificări", + "LOGIN": "Conectare și acces", + "DOMAIN": "Domeniu", + "TEXTS": "Texte și limbi", + "APPEARANCE": "Aspect", + "OTHER": "Altele", + "STORAGE": "Stocare" + } + }, + "SETTING": { + "LANGUAGES": { + "DEFAULT": "Limbă implicită", + "ALLOWED": "Limbi permise", + "NOT_ALLOWED": "Limbi nepermise", + "ALLOW_ALL": "Permiteți toate", + "DISALLOW_ALL": "Nu permiteți toate", + "SETASDEFAULT": "Setați ca limbă implicită", + "DEFAULT_SAVED": "Limba implicită a fost salvată", + "ALLOWED_SAVED": "Limbile permise au fost salvate", + "OPTIONS": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" + } + }, + "SMTP": { + "TITLE": "Furnizor SMTP", + "DESCRIPTION": "Descriere", + "SENDERADDRESS": "Adresă de e-mail expeditor", + "SENDERNAME": "Nume expeditor", + "REPLYTOADDRESS": "Adresă de răspuns", + "HOSTANDPORT": "Gazdă și port", + "USER": "Utilizator", + "PASSWORD": "Parolă", + "SETPASSWORD": "Setați parola SMTP", + "PASSWORDSET": "Parola SMTP a fost setată cu succes.", + "TLS": "Securitatea stratului de transport (TLS)", + "SAVED": "Salvat cu succes!", + "NOCHANGES": "Nicio modificare!", + "REQUIREDWARN": "Pentru a trimite notificări din domeniul dvs., trebuie să introduceți datele SMTP." + }, + "SMS": { + "PROVIDERS": "Furnizori", + "PROVIDER": "Furnizor SMS", + "ADDPROVIDER": "Adăugați furnizor SMS", + "ADDPROVIDERDESCRIPTION": "Alegeți unul dintre furnizorii disponibili și introduceți datele necesare.", + "REMOVEPROVIDER": "Eliminați furnizorul", + "REMOVEPROVIDER_DESC": "Sunteți pe cale să ștergeți o configurație de furnizor. Doriți să continuați?", + "SMSPROVIDERSTATE": { + "0": "Nespecificat", + "1": "Activ", + "2": "Inactiv" + }, + "ACTIVATED": "Furnizor activat.", + "DEACTIVATED": "Furnizor dezactivat.", + "TWILIO": { + "SID": "Sid", + "TOKEN": "Token", + "SENDERNUMBER": "Număr expeditor", + "VERIFYSERVICESID": "Verification Service Sid", + "VERIFYSERVICESID_DESCRIPTION": "Setarea unui Verification Service Sid, permite utilizarea Twilio Verify Service în loc de Messages Service pentru verificarea numerelor de telefon și a OTP SMS", + "ADDED": "Twilio adăugat cu succes.", + "UPDATED": "Twilio actualizat cu succes.", + "REMOVED": "Twilio eliminat", + "CHANGETOKEN": "Modificați tokenul", + "SETTOKEN": "Setați tokenul", + "TOKENSET": "Token setat cu succes." + } + }, + "SECRETS": { + "TYPES": "Tipuri de secrete", + "TYPE": { + "1": "E-mail de inițializare", + "2": "Verificarea e-mailului", + "3": "Verificarea telefonului", + "4": "Resetarea parolei", + "5": "Inițializarea fără parolă", + "6": "Secretul aplicației", + "7": "Parolă unică (OTP) - SMS", + "8": "Parolă unică (OTP) - E-mail" + }, + "EXPIRY": "Expirare (în minute)", + "INCLUDEDIGITS": "Includeți numere", + "INCLUDESYMBOLS": "Includeți simboluri", + "INCLUDELOWERLETTERS": "Includeți litere mici", + "INCLUDEUPPERLETTERS": "Includeți litere mari", + "LENGTH": "Lungime", + "UPDATED": "Setări actualizate." + }, + "SECURITY": { + "IFRAMETITLE": "iFrame", + "IFRAMEDESCRIPTION": "Această setare setează CSP pentru a permite încadrarea dintr-un set de domenii permise. Rețineți că prin activarea utilizării iFrames, vă asumați riscul de a permite clickjacking.", + "IFRAMEENABLED": "Permiteți iFrame", + "ALLOWEDORIGINS": "Adrese URL permise", + "IMPERSONATIONTITLE": "Impersonare", + "IMPERSONATIONENABLED": "Permiteți impersonarea", + "IMPERSONATIONDESCRIPTION": "Această setare permite utilizarea impersonării în principiu. Rețineți că impersonatorul trebuie să aibă și rolurile *_IMPERSONATOR atribuite corespunzător." + }, + "FEATURES": { + "LOGINDEFAULTORG": "Organizație implicită de conectare", + "LOGINDEFAULTORG_DESCRIPTION": "UI-ul de conectare va utiliza setările organizației implicite (și nu din instanță) dacă nu este setat niciun context de organizație", + "OIDCLEGACYINTROSPECTION": "Introspecție OIDC Legacy", + "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Am refactorizat recent endpointul de introspecție din motive de performanță. Această caracteristică poate fi utilizată pentru a reveni la implementarea legacy dacă apar erori neașteptate.", + "OIDCTOKENEXCHANGE": "Schimb de token OIDC", + "OIDCTOKENEXCHANGE_DESCRIPTION": "Activați tipul de grant experimental urn:ietf:params:oauth:grant-type:token-exchange pentru endpointul token OIDC. Schimbul de tokenuri poate fi utilizat pentru a solicita tokenuri cu o rază de acțiune mai mică sau pentru a impersona alți utilizatori. Consultați politica de securitate pentru a permite impersonarea pe o instanță.", + "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Proiecții de introspecție OIDC Trigger", + "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Activați declanșatoarele de proiecție în timpul unei solicitări de introspecție. Acest lucru poate acționa ca o soluție dacă există probleme notabile de consistență în răspunsul de introspecție, dar poate avea un impact asupra performanței. Planificăm să eliminăm declanșatoarele pentru solicitările de introspecție în viitor.", + "USERSCHEMA": "Schema de utilizator", + "USERSCHEMA_DESCRIPTION": "Schemele de utilizator permit gestionarea schemelor de date ale utilizatorului. Dacă indicatorul este activat, veți putea utiliza noul API și caracteristicile sale.", + "ACTIONS": "Acțiuni", + "ACTIONS_DESCRIPTION": "Acțiunile v2 permit gestionarea execuțiilor și țintelor de date. Dacă indicatorul este activat, veți putea utiliza noul API și caracteristicile sale.", + "OIDCSINGLEV1SESSIONTERMINATION": "Terminarea sesiunii unice OIDC V1", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Dacă indicatorul este activat, veți putea termina o singură sesiune din UI-ul de conectare furnizând un id_token cu o revendicare sid ca id_token_hint pe endpointul end_session. Rețineți că în prezent toate sesiunile de la același agent utilizator (browser) sunt terminate în UI-ul de conectare. Sesiunile gestionate prin API-ul Session permit deja terminarea sesiunilor individuale.", + "DEBUGOIDCPARENTERROR": "Eroare de Depanare Părinte OIDC", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Dacă steagul este activat, eroarea părinte OIDC va fi înregistrată în consolă.", + "DISABLEUSERTOKENEVENT": "Dezactivează Evenimentul Token Utilizator", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Activează Logout Backchannel", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Logout-ul Back-Channel implementează OpenID Connect Back-Channel Logout 1.0 și poate fi folosit pentru a notifica clienții despre terminarea sesiunii la Producătorul OpenID.", + "PERMISSIONCHECKV2": "Verificare Permisiuni V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Dacă steagul este activat, veți putea folosi noua API și funcțiile sale.", + "WEBKEY": "Cheie Web", + "WEBKEY_DESCRIPTION": "Dacă steagul este activat, veți putea folosi noua API și funcțiile sale.", + "STATES": { + "INHERITED": "Moșteniți", + "ENABLED": "Activat", + "DISABLED": "Dezactivat" + }, + "INHERITED_DESCRIPTION": "Aceasta setează valoarea la valoarea implicită a sistemului.", + "INHERITEDINDICATOR_DESCRIPTION": { + "ENABLED": "\"Activat\" este moștenit", + "DISABLED": "\"Dezactivat\" este moștenit" + }, + "RESET": "Setați totul pentru a moșteni", + "CONSOLEUSEV2USERAPI": "Utilizați API-ul V2 în Consola pentru crearea utilizatorului", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Când acest indicator este activat, consola utilizează API-ul de utilizator V2 pentru a crea utilizatori noi. Cu API-ul V2, utilizatorii nou creați încep fără o stare inițială.", + "LOGINV2": "Autentificare V2", + "LOGINV2_DESCRIPTION": "Activarea acestei opțiuni pornește noua interfață de autentificare bazată pe TypeScript, cu securitate, performanță și personalizare îmbunătățite.", + "LOGINV2_BASEURI": "URI de bază" + }, + "DIALOG": { + "RESET": { + "DEFAULTTITLE": "Resetați setarea", + "DEFAULTDESCRIPTION": "Sunteți pe cale să vă resetați setările la configurația implicită a instanței dvs. Sigur doriți să continuați?", + "LOGINPOLICY_DESCRIPTION": "Avertisment: dacă continuați, setările furnizorului de identitate vor fi resetate și la setările instanței." + } + } + }, + "POLICY": { + "APPLIEDTO": "Aplicat la", + "PWD_COMPLEXITY": { + "TITLE": "Complexitatea parolei", + "DESCRIPTION": "Asigură că toate parolele setate corespund unui model specific", + "SYMBOLANDNUMBERERROR": "Trebuie să conțină o cifră și un simbol/semn de punctuație.", + "SYMBOLERROR": "Trebuie să includă un simbol/semn de punctuație.", + "NUMBERERROR": "Trebuie să includă o cifră.", + "PATTERNERROR": "Parola nu respectă modelul cerut." + }, + "NOTIFICATION": { + "TITLE": "Notificare", + "DESCRIPTION": "Determină la ce modificări vor fi trimise notificări.", + "PASSWORDCHANGE": "Modificarea parolei" + }, + "PRIVATELABELING": { + "DESCRIPTION": "Oferiți conectării stilul dvs. personalizat și modificați-i comportamentul.", + "PREVIEW_DESCRIPTION": "Modificările politicii vor fi implementate automat în mediul de previzualizare.", + "BTN": "Selectați fișier", + "ACTIVATEPREVIEW": "Aplicați configurația", + "DARK": "Mod întunecat", + "LIGHT": "Mod luminos", + "CHANGEVIEW": "Modificați vizualizarea", + "ACTIVATED": "Modificările politicii sunt acum LIVE", + "THEME": "Temă", + "COLORS": "Culori", + "FONT": "Font", + "ADVANCEDBEHAVIOR": "Comportament avansat", + "DROP": "Trageți imaginea aici sau", + "RELEASE": "Eliberați", + "DROPFONT": "Trageți fișierul fontului aici", + "RELEASEFONT": "Eliberați", + "USEOFLOGO": "Logo-ul dvs. va fi utilizat în conectare, precum și în e-mailuri, în timp ce pictograma este utilizată pentru elemente UI mai mici, cum ar fi în comutatorul organizației în consolă", + "MAXSIZE": "Dimensiunea maximă este limitată la 524 kB", + "EMAILNOSVG": "Formatul de fișier SVG nu este acceptat în e-mailuri. Prin urmare, încărcați-vă logo-ul în PNG sau în alt format acceptat.", + "MAXSIZEEXCEEDED": "Dimensiune maximă de 524 kB depășită.", + "NOSVGSUPPORTED": "SVG-urile nu sunt acceptate!", + "FONTINLOGINONLY": "Fontul este afișat în prezent numai în interfața de conectare.", + "BACKGROUNDCOLOR": "Culoare de fundal", + "PRIMARYCOLOR": "Culoare primară", + "WARNCOLOR": "Culoare de avertisment", + "FONTCOLOR": "Culoare font", + "VIEWS": { + "PREVIEW": "Previzualizare", + "CURRENT": "Configurația curentă" + }, + "PREVIEW": { + "TITLE": "Conectare", + "SECOND": "conectați-vă cu contul dvs. ZITADEL.", + "ERROR": "Utilizatorul nu a putut fi găsit!", + "PRIMARYBUTTON": "următorul", + "SECONDARYBUTTON": "înregistrați-vă" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Mod automat", + "THEME_MODE_LIGHT": "Numai mod luminos", + "THEME_MODE_DARK": "Numai mod întunecat" + } + }, + "PWD_AGE": { + "TITLE": "Expirarea parolei", + "DESCRIPTION": "Puteți seta o politică pentru expirarea parolelor. Această politică va obliga utilizatorul să își schimbe parola la următoarea conectare după expirare. Nu există avertismente și notificări automate." + }, + "PWD_LOCKOUT": { + "TITLE": "Politica de blocare", + "DESCRIPTION": "Setați un număr maxim de încercări de parolă, după care conturile vor fi blocate." + }, + "PRIVATELABELING_POLICY": { + "TITLE": "Branding", + "BTN": "Selectați fișier", + "DESCRIPTION": "Personalizați aspectul conectării", + "ACTIVATEPREVIEW": "Activați configurația" + }, + "LOGIN_POLICY": { + "TITLE": "Setări de conectare", + "DESCRIPTION": "Definiți modul în care utilizatorii pot fi autentificați și configurați furnizorii de identitate", + "DESCRIPTIONCREATEADMIN": "Utilizatorii pot alege dintre furnizorii de identitate disponibili de mai jos.", + "DESCRIPTIONCREATEMGMT": "Utilizatorii pot alege dintre furnizorii de identitate disponibili de mai jos. Notă: Puteți utiliza atât furnizorii setați de sistem, cât și furnizorii setați numai pentru organizația dvs.", + "LIFETIME_INVALID": "Formularul conține valori nevalide.", + "SAVED": "Salvat cu succes!", + "PROVIDER_ADDED": "Furnizorul de identitate a fost activat." + }, + "PRIVACY_POLICY": { + "DESCRIPTION": "Setați linkurile către Politica dvs. de confidențialitate și Termenii și condițiile", + "TOSLINK": "Link către Termenii și condițiile", + "POLICYLINK": "Link către Politica de confidențialitate", + "HELPLINK": "Link către Ajutor", + "SUPPORTEMAIL": "E-mail de asistență", + "DOCSLINK": "Link către Documente (Consolă)", + "CUSTOMLINK": "Link personalizat (Consolă)", + "CUSTOMLINKTEXT": "Text link personalizat (Consolă)", + "SAVED": "Salvat cu succes!", + "RESET_TITLE": "Restabiliți valorile implicite", + "RESET_DESCRIPTION": "Sunteți pe cale să restabiliți linkurile implicite pentru TOS și Politica de confidențialitate. Doriți cu adevărat să continuați?" + }, + "LOGIN_TEXTS": { + "TITLE": "Texte interfață de conectare", + "DESCRIPTION": "Definiți-vă textele pentru interfețele de conectare. Dacă textele sunt goale, va fi utilizată valoarea implicită afișată ca substituent.", + "DESCRIPTION_SHORT": "Definiți-vă textele pentru interfețele de conectare.", + "NEWERVERSIONEXISTS": "Există o versiune mai nouă", + "CURRENTDATE": "Configurația curentă", + "CHANGEDATE": "Versiune mai nouă de la", + "KEYNAME": "Ecran / Interfață de conectare", + "RESET_TITLE": "Restabiliți valorile implicite", + "RESET_DESCRIPTION": "Sunteți pe cale să restabiliți toate valorile implicite. Toate modificările pe care le-ați făcut vor fi șterse definitiv. Doriți cu adevărat să continuați?", + "UNSAVED_TITLE": "Continuați fără a salva?", + "UNSAVED_DESCRIPTION": "Ați făcut modificări fără a salva. Doriți să salvați acum?", + "ACTIVE_LANGUAGE_NOT_ALLOWED": "Ați selectat o limbă care nu este permisă. Puteți continua să modificați textele. Dar dacă doriți ca utilizatorii dvs. să poată utiliza efectiv această limbă, modificați restricțiile instanței dvs.", + "LANGUAGES_NOT_ALLOWED": "Nepermise:", + "LANGUAGE": "Limbă", + "LANGUAGES": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" + }, + "KEYS": { + "emailVerificationDoneText": "Verificarea e-mailului efectuată", + "emailVerificationText": "Verificarea e-mailului", + "externalUserNotFoundText": "Utilizatorul extern nu a fost găsit", + "footerText": "Subsol", + "initMfaDoneText": "MFA inițializat efectuată", + "initMfaOtpText": "Inițializați MFA", + "initMfaPromptText": "Solicitare inițializare MFA", + "initMfaU2fText": "Inițializați al doilea factor universal", + "initPasswordDoneText": "Parola inițializată efectuată", + "initPasswordText": "Inițializați parola", + "initializeDoneText": "Utilizatorul inițializat efectuat", + "initializeUserText": "Inițializați utilizatorul", + "linkingUserDoneText": "Utilizatorul de conectare efectuat", + "loginText": "Conectare", + "logoutText": "Deconectare", + "mfaProvidersText": "Furnizori MFA", + "passwordChangeDoneText": "Modificarea parolei efectuată", + "passwordChangeText": "Modificarea parolei", + "passwordResetDoneText": "Resetarea parolei efectuată", + "passwordText": "Parolă", + "registrationOptionText": "Opțiuni de înregistrare", + "registrationOrgText": "Înregistrați organizația", + "registrationUserText": "Înregistrați utilizatorul", + "selectAccountText": "Selectați contul", + "successLoginText": "Conectare cu succes", + "usernameChangeDoneText": "Modificarea numelui de utilizator efectuată", + "usernameChangeText": "Modificarea numelui de utilizator", + "verifyMfaOtpText": "Verificați OTP", + "verifyMfaU2fText": "Verificați al doilea factor universal", + "passwordlessPromptText": "Solicitare fără parolă", + "passwordlessRegistrationDoneText": "Înregistrarea fără parolă efectuată", + "passwordlessRegistrationText": "Înregistrarea fără parolă", + "passwordlessText": "Fără parolă", + "externalRegistrationUserOverviewText": "Prezentare generală utilizator înregistrare externă" + } + }, + "MESSAGE_TEXTS": { + "TYPE": "Notificare", + "TYPES": { + "INIT": "Inițializare", + "VE": "Verificați e-mailul", + "VP": "Verificați telefonul", + "VSO": "Verificați OTP SMS", + "VEO": "Verificați OTP e-mail", + "PR": "Resetare parolă", + "DC": "Revendicare domeniu", + "PL": "Fără parolă", + "PC": "Modificare parolă", + "IU": "Invitați utilizatorul" + }, + "CHIPS": { + "firstname": "Prenume", + "lastname": "Nume de familie", + "code": "Cod", + "preferredLoginName": "Nume de conectare preferat", + "displayName": "Nume afișat", + "nickName": "Poreclă", + "loginnames": "Nume de conectare", + "domain": "Domeniu", + "lastEmail": "Ultimul e-mail", + "lastPhone": "Ultimul telefon", + "verifiedEmail": "E-mail verificat", + "verifiedPhone": "Telefon verificat", + "changedate": "Data modificării", + "username": "Nume de utilizator", + "tempUsername": "Nume de utilizator temporar", + "otp": "Parolă unică", + "verifyUrl": "Verificați adresa URL a parolei unice", + "expiry": "Expirare", + "applicationName": "Numele aplicației" + }, + "TOAST": { + "UPDATED": "Textele personalizate au fost salvate." + } + }, + "DEFAULTLABEL": "Setările curente corespund standardului instanței dvs.", + "BTN_INSTALL": "Configurați", + "BTN_EDIT": "Modificați", + "DATA": { + "DESCRIPTION": "Descriere", + "MINLENGTH": "trebuie să aibă o lungime minimă", + "HASNUMBER": "trebuie să includă un număr", + "HASSYMBOL": "trebuie să includă un simbol", + "HASLOWERCASE": "trebuie să includă o literă mică", + "HASUPPERCASE": "trebuie să includă o literă mare", + "SHOWLOCKOUTFAILURES": "afișați erorile de blocare", + "MAXPASSWORDATTEMPTS": "Numărul maxim de încercări de parolă", + "MAXOTPATTEMPTS": "Numărul maxim de încercări OTP", + "EXPIREWARNDAYS": "Avertisment de expirare după zile", + "MAXAGEDAYS": "Valabilitate maximă în zile", + "USERLOGINMUSTBEDOMAIN": "Adăugați domeniul organizației ca sufix la numele de conectare", + "USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Dacă activați această setare, toate numele de conectare vor fi sufixate cu domeniul organizației. Dacă această setare este dezactivată, trebuie să vă asigurați că numele de utilizator sunt unice pentru toate organizațiile.", + "VALIDATEORGDOMAINS": "Verificarea domeniului organizației este necesară (provocare DNS sau HTTP)", + "SMTPSENDERADDRESSMATCHESINSTANCEDOMAIN": "Adresa expeditorului SMTP se potrivește cu domeniul instanței", + "ALLOWUSERNAMEPASSWORD_DESC": "Conectarea convențională cu nume de utilizator și parolă este permisă.", + "ALLOWEXTERNALIDP_DESC": "Conectarea este permisă pentru furnizorii de identitate de bază", + "ALLOWREGISTER_DESC": "Dacă opțiunea este selectată, în procesul de conectare apare un pas suplimentar pentru înregistrarea unui utilizator.", + "FORCEMFA": "Forțați MFA pentru toți utilizatorii", + "FORCEMFALOCALONLY": "Forțați MFA numai pentru utilizatorii autentificați local", + "FORCEMFALOCALONLY_DESC": "Dacă opțiunea este selectată, utilizatorii autentificați local trebuie să configureze un al doilea factor pentru conectare.", + "HIDEPASSWORDRESET_DESC": "Dacă opțiunea este selectată, utilizatorul nu își poate reseta parola în procesul de conectare.", + "HIDELOGINNAMESUFFIX": "Ascundeți sufixul numelui de conectare", + "HIDELOGINNAMESUFFIX_DESC": "Ascunde sufixul numelui de conectare în interfața de conectare", + "IGNOREUNKNOWNUSERNAMES_DESC": "Dacă opțiunea este selectată, ecranul cu parolă va fi afișat în procesul de conectare chiar dacă utilizatorul nu a fost găsit. Eroarea la verificarea parolei nu va dezvălui dacă numele de utilizator sau parola au fost greșite.", + "ALLOWDOMAINDISCOVERY_DESC": "Dacă opțiunea este selectată, sufixul (@domain.com) al unei intrări de nume de utilizator necunoscut pe ecranul de conectare va fi comparat cu domeniile organizației și va redirecționa către înregistrarea organizației respective în caz de succes.", + "DEFAULTREDIRECTURI": "URI implicit de redirecționare", + "DEFAULTREDIRECTURI_DESC": "Definește unde va fi redirecționat utilizatorul dacă conectarea a început fără un context de aplicație (de exemplu, din e-mail)", + "ERRORMSGPOPUP": "Afișați mesajul de eroare într-o casetă de dialog", + "DISABLEWATERMARK": "Ascundeți filigranul", + "DISABLEWATERMARK_DESC": "Ascundeți filigranul Alimentat de ZITADEL în interfața de conectare" + }, + "RESET": "Resetați la valoarea implicită a instanței", + "CREATECUSTOM": "Creați o politică personalizată", + "TOAST": { + "SET": "Politica a fost setată cu succes!", + "RESETSUCCESS": "Politica a fost resetată cu succes!", + "UPLOADSUCCESS": "Încărcat cu succes!", + "DELETESUCCESS": "Șters cu succes!", + "UPLOADFAILED": "Încărcarea a eșuat!" + } + }, + "ORG_DETAIL": { + "TITLE": "Organizație", + "DESCRIPTION": "Aici puteți edita configurația organizației dvs. și puteți gestiona membrii.", + "DETAIL": { + "TITLE": "Detalii", + "NAME": "Nume", + "DOMAIN": "Domeniu", + "STATE": { + "0": "Nedefinit", + "1": "Activ", + "2": "Inactiv" + } + }, + "MEMBER": { + "TITLE": "Membri", + "USERNAME": "Nume de utilizator", + "DISPLAYNAME": "Nume afișat", + "LOGINNAME": "Nume de conectare", + "EMAIL": "E-mail", + "ROLES": "Roluri", + "ADD": "Adăugați membru", + "ADDDESCRIPTION": "Introduceți numele utilizatorilor care urmează să fie adăugați." + }, + "TABLE": { + "TOTAL": "Număr total de intrări", + "SELECTION": "Elemente selectate", + "DEACTIVATE": "Dezactivați utilizatorul", + "ACTIVATE": "Activați utilizatorul", + "DELETE": "Ștergeți utilizatorul", + "CLEAR": "Goliți selecția" + } + }, + "PROJECT": { + "PAGES": { + "TITLE": "Proiect", + "DESCRIPTION": "Aici puteți defini aplicații, puteți gestiona rolurile și puteți acorda altor organizații să utilizeze proiectul dvs.", + "DELETE": "Ștergeți proiectul", + "DETAIL": "Detalii", + "CREATE": "Creați proiectul", + "CREATE_DESC": "Introduceți numele proiectului dvs.", + "ROLE": "Rol", + "NOITEMS": "Niciun proiect", + "ZITADELPROJECT": "Acesta aparține proiectului ZITADEL. Atenție: Dacă faceți modificări, ZITADEL ar putea să nu se comporte conform intențiilor.", + "TYPE": { + "OWNED": "Proiecte deținute", + "OWNED_SINGULAR": "Proiect deținut", + "GRANTED_SINGULAR": "Proiect acordat al {{name}}" + }, + "PRIVATELABEL": { + "TITLE": "Setare de branding", + "0": { + "TITLE": "Nespecificat", + "DESC": "De îndată ce utilizatorul este identificat, brandingul organizației utilizatorului identificat va fi afișat înainte ca valoarea implicită a sistemului să fie afișată." + }, + "1": { + "TITLE": "Utilizați setarea proiectului", + "DESC": "Va fi afișat brandingul organizației care deține proiectul" + }, + "2": { + "TITLE": "Utilizați setarea Organizației utilizatorului", + "DESC": "Va fi afișat brandingul organizației proiectului, dar de îndată ce utilizatorul este identificat, va fi afișată setarea organizației utilizatorului identificat." + }, + "DIALOG": { + "TITLE": "Setare branding", + "DESCRIPTION": "Selectați comportamentul conectării, atunci când utilizați proiectul." + } + }, + "PINNED": "Fixat", + "ALL": "Toate", + "CREATEDON": "Creat la", + "LASTMODIFIED": "Ultima modificare la", + "ADDNEW": "Creați un proiect nou", + "DIALOG": { + "REACTIVATE": { + "TITLE": "Reactivați proiectul", + "DESCRIPTION": "Doriți cu adevărat să vă reactivați proiectul?" + }, + "DEACTIVATE": { + "TITLE": "Dezactivați proiectul", + "DESCRIPTION": "Doriți cu adevărat să vă dezactivați proiectul?" + }, + "DELETE": { + "TITLE": "Ștergeți proiectul", + "DESCRIPTION": "Doriți cu adevărat să vă ștergeți proiectul?", + "TYPENAME": "Tastați numele proiectului pentru a-l șterge definitiv." + } + } + }, + "SETTINGS": { + "TITLE": "Setări", + "DESCRIPTION": "" + }, + "STATE": { + "TITLE": "Stare", + "0": "Nedefinit", + "1": "Activ", + "2": "Inactiv" + }, + "TYPE": { + "TITLE": "Tip", + "0": "Tip necunoscut", + "1": "Deținut", + "2": "Acordat" + }, + "NAME": "Nume", + "NAMEDIALOG": { + "TITLE": "Redenumiți proiectul", + "DESCRIPTION": "Introduceți noul nume pentru proiectul dvs.", + "NAME": "Nume nou" + }, + "MEMBER": { + "TITLE": "Manageri", + "TITLEDESC": "Managerii pot face modificări acestui proiect pe baza rolului lor.", + "DESCRIPTION": "Acești manageri ar putea să vă poată edita proiectul.", + "USERNAME": "Nume de utilizator", + "DISPLAYNAME": "Nume afișat", + "LOGINNAME": "Nume de conectare", + "EMAIL": "E-mail", + "ROLES": "Roluri", + "USERID": "ID utilizator" + }, + "GRANT": { + "EMPTY": "Nicio organizație acordată.", + "TITLE": "Granturi de proiect", + "DESCRIPTION": "Permiteți altei organizații să vă utilizeze proiectul.", + "EDITTITLE": "Editați rolurile", + "CREATE": { + "TITLE": "Creați un grant de organizație", + "SEL_USERS": "Selectați utilizatorii cărora doriți să le acordați acces", + "SEL_PROJECT": "Căutați un proiect", + "SEL_ROLES": "Selectați rolurile pe care doriți să le adăugați la grant", + "SEL_USER": "Selectați utilizatori", + "SEL_ORG": "Căutați o organizație", + "SEL_ORG_DESC": "Căutați organizația căreia să acordați.", + "ORG_DESCRIPTION": "Sunteți pe cale să acordați un utilizator pentru organizația {{name}}.", + "ORG_DESCRIPTION_DESC": "Schimbați contextul în antetul de mai sus pentru a acorda un utilizator pentru o altă organizație.", + "SEL_ORG_FORMFIELD": "Organizație", + "FOR_ORG": "Grantul este creat pentru:" + }, + "DETAIL": { + "TITLE": "Grant de proiect", + "DESC": "Puteți selecta ce roluri pot fi utilizate de organizația specificată și puteți alege manageri", + "MEMBERTITLE": "Manageri", + "MEMBERDESC": "Aceștia sunt managerii organizației acordate. Adăugați aici utilizatori care ar trebui să obțină acces pentru a edita datele proiectului.", + "PROJECTNAME": "Numele proiectului", + "GRANTEDORG": "Organizație acordată", + "RESOURCEOWNER": "Proprietarul resursei" + }, + "STATE": "Stare", + "STATES": { + "1": "Activ", + "2": "Inactiv" + }, + "ALL": "Toate", + "SHOWDETAIL": "Afișați detalii", + "USER": "Utilizator", + "MEMBERS": "Manageri", + "ORG": "Organizație", + "PROJECTNAME": "Numele proiectului", + "GRANTEDORG": "Organizație acordată", + "GRANTEDORGDOMAIN": "Domeniu", + "RESOURCEOWNER": "Proprietarul resursei", + "GRANTEDORGNAME": "Nume organizație", + "GRANTID": "Id-ul grantului", + "CREATIONDATE": "Data creării", + "CHANGEDATE": "Ultima modificare", + "DATES": "Date", + "ROLENAMESLIST": "Roluri", + "NOROLES": "Niciun rol", + "TYPE": "Tip", + "TOAST": { + "PROJECTGRANTUSERGRANTADDED": "Grantul de proiect a fost creat.", + "PROJECTGRANTADDED": "Grantul de proiect a fost creat.", + "PROJECTGRANTCHANGED": "Grantul de proiect a fost modificat.", + "PROJECTGRANTMEMBERADDED": "Managerul grantului a fost adăugat.", + "PROJECTGRANTMEMBERCHANGED": "Managerul grantului a fost modificat.", + "PROJECTGRANTMEMBERREMOVED": "Managerul grantului a fost eliminat.", + "PROJECTGRANTUPDATED": "Grantul de proiect a fost actualizat" + }, + "DIALOG": { + "DELETE_TITLE": "Ștergeți grantul de proiect", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un grant de proiect. Sigur doriți să continuați?" + }, + "ROLES": "Roluri de proiect" + }, + "APP": { + "TITLE": "Aplicații", + "NAME": "Nume", + "NAMEREQUIRED": "Este necesar un nume." + }, + "ROLE": { + "EMPTY": "Niciun rol nu a fost creat încă.", + "ADDNEWLINE": "Adăugați un rol suplimentar", + "KEY": "Cheie", + "TITLE": "Roluri", + "DESCRIPTION": "Definiți câteva roluri care pot fi utilizate pentru a crea granturi de proiect.", + "NAME": "Nume", + "DISPLAY_NAME": "Nume afișat", + "GROUP": "Grup", + "ACTIONS": "Acțiuni", + "ADDTITLE": "Creați rolul", + "ADDDESCRIPTION": "Introduceți datele pentru noul rol.", + "EDITTITLE": "Editați rolul", + "EDITDESCRIPTION": "Introduceți datele noi pentru rol.", + "DELETE": "Ștergeți rolul", + "CREATIONDATE": "Creat", + "CHANGEDATE": "Ultima modificare", + "SELECTGROUPTOOLTIP": "Selectați toate rolurile grupului {{group}}.", + "OPTIONS": "Opțiuni", + "ASSERTION": "Afirmați rolurile la autentificare", + "ASSERTION_DESCRIPTION": "Informațiile despre roluri sunt trimise din endpoint-ul Userinfo și, în funcție de setările aplicației dvs., în tokenuri și alte tipuri.", + "CHECK": "Verificați autorizarea la autentificare", + "CHECK_DESCRIPTION": "Dacă este setat, utilizatorilor li se permite să se autentifice numai dacă oricărui cont le este atribuit vreun rol.", + "DIALOG": { + "DELETE_TITLE": "Ștergeți rolul", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un rol de proiect. Sigur doriți să continuați?" + } + }, + "HAS_PROJECT": "Verificați proiectul la autentificare", + "HAS_PROJECT_DESCRIPTION": "Se verifică dacă organizația utilizatorului are acest proiect. Dacă nu, utilizatorul nu se poate autentifica.", + "TABLE": { + "TOTAL": "Numărul total de intrări:", + "SELECTION": "Elemente selectate", + "DEACTIVATE": "Dezactivați proiectul", + "ACTIVATE": "Activați proiectul", + "DELETE": "Ștergeți proiectul", + "ORGNAME": "Nume organizație", + "ORGDOMAIN": "Domeniu organizație", + "STATE": "Stare", + "TYPE": "Tip", + "CREATIONDATE": "Creat la", + "CHANGEDATE": "Ultima modificare", + "RESOURCEOWNER": "Proprietar", + "SHOWTABLE": "Afișați tabelul", + "SHOWGRID": "Afișați grila", + "EMPTY": "Niciun proiect găsit" + }, + "TOAST": { + "MEMBERREMOVED": "Manager eliminat.", + "MEMBERSADDED": "Manageri adăugați.", + "MEMBERADDED": "Manager adăugat.", + "MEMBERCHANGED": "Manager modificat.", + "ROLESCREATED": "Roluri create.", + "ROLEREMOVED": "Rol eliminat.", + "ROLECHANGED": "Rol modificat.", + "REACTIVATED": "Reactivat.", + "DEACTIVATED": "Dezactivat.", + "CREATED": "Proiect creat.", + "UPDATED": "Proiect modificat.", + "GRANTUPDATED": "Grant modificat.", + "DELETED": "Proiect șters." + } + }, + "ROLES": { + "DIALOG": { + "DELETE_TITLE": "Ștergeți rolul", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un rol. Sigur doriți să continuați?" + } + }, + "NEXTSTEPS": { + "TITLE": "Pași următori" + }, + "IDP": { + "LIST": { + "ACTIVETITLE": "Furnizori de identitate activi" + }, + "CREATE": { + "TITLE": "Adăugați furnizor", + "DESCRIPTION": "Selectați unul sau mai mulți dintre următorii furnizori.", + "STEPPERTITLE": "Creați furnizorul", + "OIDC": { + "TITLE": "Furnizor OIDC", + "DESCRIPTION": "Introduceți datele necesare pentru furnizorul dvs. OIDC." + }, + "OAUTH": { + "TITLE": "Furnizor OAuth", + "DESCRIPTION": "Introduceți datele necesare pentru furnizorul dvs. OAuth." + }, + "JWT": { + "TITLE": "Furnizor JWT", + "DESCRIPTION": "Introduceți datele necesare pentru furnizorul dvs. JWT." + }, + "GOOGLE": { + "TITLE": "Furnizor Google", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Google" + }, + "GITLAB": { + "TITLE": "Furnizor Gitlab", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Gitlab" + }, + "GITLABSELFHOSTED": { + "TITLE": "Furnizor Gitlab autogăzduit", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Gitlab autogăzduit" + }, + "GITHUBES": { + "TITLE": "Furnizor GitHub Enterprise Server", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate GitHub Enterprise Server" + }, + "GITHUB": { + "TITLE": "Furnizor Github", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Github" + }, + "AZUREAD": { + "TITLE": "Furnizor Microsoft", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. de identitate Microsoft" + }, + "LDAP": { + "TITLE": "Active Directory / LDAP", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. LDAP" + }, + "APPLE": { + "TITLE": "Conectare cu Apple", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. Apple" + }, + "SAML": { + "TITLE": "Conectare cu SAML", + "DESCRIPTION": "Introduceți acreditările pentru furnizorul dvs. SAML" + } + }, + "DETAIL": { + "TITLE": "Furnizor de identitate", + "DESCRIPTION": "Actualizați configurația furnizorului dvs.", + "DATECREATED": "Creat", + "DATECHANGED": "Modificat" + }, + "OPTIONS": { + "ISAUTOCREATION": "Creare automată", + "ISAUTOCREATION_DESC": "Dacă este selectat, va fi creat un cont dacă nu există încă.", + "ISAUTOUPDATE": "Actualizare automată", + "ISAUTOUPDATE_DESC": "Dacă este selectat, conturile sunt actualizate la reautentificare.", + "ISCREATIONALLOWED": "Crearea contului permisă (manual)", + "ISCREATIONALLOWED_DESC": "Determină dacă conturile pot fi create folosind un cont extern. Dezactivați dacă utilizatorii nu ar trebui să poată edita informațiile contului când auto_creation este activată.", + "ISLINKINGALLOWED": "Conectarea contului permisă (manual)", + "ISLINKINGALLOWED_DESC": "Determină dacă o identitate poate fi conectată manual la un cont existent. Dezactivați dacă utilizatorilor ar trebui să li se permită să conecteze doar contul propus în caz de auto_linking activ.", + "AUTOLINKING_DESC": "Determină dacă unei identități i se va solicita să fie conectată la un cont existent.", + "AUTOLINKINGTYPE": { + "0": "Dezactivat", + "1": "Verificați dacă există un nume de utilizator existent", + "2": "Verificați dacă există un e-mail existent" + } + }, + "OWNERTYPES": { + "0": "necunoscut", + "1": "Instanță", + "2": "Organizație" + }, + "STATES": { + "1": "activ", + "2": "inactiv" + }, + "AZUREADTENANTTYPES": { + "3": "ID chiriaș", + "0": "Comun", + "1": "Organizații", + "2": "Consumatori" + }, + "AZUREADTENANTTYPE": "Tip chiriaș", + "AZUREADTENANTID": "ID chiriaș", + "EMAILVERIFIED": "E-mail verificat", + "NAMEHINT": "Dacă este specificat, va fi afișat în interfața de conectare.", + "OPTIONAL": "opțional", + "LDAPATTRIBUTES": "Atribute LDAP", + "UPDATEBINDPASSWORD": "actualizați parola de conectare", + "UPDATECLIENTSECRET": "actualizați secretul clientului", + "ADD": "Adăugați furnizor de identitate", + "TYPE": "Tip", + "OWNER": "Proprietar", + "ID": "ID", + "NAME": "Nume", + "AUTHORIZATIONENDPOINT": "Punct final de autorizare", + "TOKENENDPOINT": "Punct final de token", + "USERENDPOINT": "Punct final utilizator", + "IDATTRIBUTE": "Atribut ID", + "AVAILABILITY": "Disponibilitate", + "AVAILABLE": "disponibil", + "AVAILABLEBUTINACTIVE": "disponibil, dar inactiv", + "SETAVAILABLE": "setați ca disponibil", + "SETUNAVAILABLE": "setați ca indisponibil", + "CONFIG": "Configurație", + "STATE": "Stare", + "ISSUER": "Emitent", + "SCOPESLIST": "Listă de domenii", + "CLIENTID": "ID client", + "CLIENTSECRET": "Secret client", + "LDAPCONNECTION": "Conexiune", + "LDAPUSERBINDING": "Conectarea utilizatorului", + "BASEDN": "BaseDn", + "BINDDN": "BindDn", + "BINDPASSWORD": "Parola de conectare", + "SERVERS": "Servere", + "STARTTLS": "Porniți TLS", + "TIMEOUT": "Timeout în secunde", + "USERBASE": "Baza de utilizatori", + "USERFILTERS": "Filtre de utilizatori", + "USEROBJECTCLASSES": "Clase de obiecte utilizator", + "REQUIRED": "necesar", + "LDAPIDATTRIBUTE": "Atribut ID", + "AVATARURLATTRIBUTE": "Atribut adresa URL avatar", + "DISPLAYNAMEATTRIBUTE": "Atribut Nume afișat", + "EMAILATTRIBUTEATTRIBUTE": "Atribut Atribut e-mail", + "EMAILVERIFIEDATTRIBUTE": "Atribut E-mail verificat", + "FIRSTNAMEATTRIBUTE": "Atribut Prenume", + "LASTNAMEATTRIBUTE": "Atribut Nume de familie", + "NICKNAMEATTRIBUTE": "Atribut Poreclă", + "PHONEATTRIBUTE": "Atribut Telefon", + "PHONEVERIFIEDATTRIBUTE": "Atribut Telefon verificat", + "PREFERREDLANGUAGEATTRIBUTE": "Atribut Limbă preferată", + "PREFERREDUSERNAMEATTRIBUTE": "Atribut Nume de utilizator preferat", + "PROFILEATTRIBUTE": "Atribut Profil", + "IDPDISPLAYNAMMAPPING": "Mapare nume afișat IdP", + "USERNAMEMAPPING": "Mapare nume de utilizator", + "DATES": "Date", + "CREATIONDATE": "Creat la", + "CHANGEDATE": "Ultima modificare", + "DEACTIVATE": "Dezactivați", + "ACTIVATE": "Activați", + "DELETE": "Ștergeți", + "DELETE_TITLE": "Ștergeți IdP", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți un furnizor de identitate. Modificările rezultate sunt irevocabile. Doriți cu adevărat să faceți acest lucru?", + "REMOVE_WARN_TITLE": "Eliminați IdP", + "REMOVE_WARN_DESCRIPTION": "Sunteți pe cale să eliminați un furnizor de identitate. Aceasta va elimina selecția IdP-ului disponibil pentru utilizatorii dvs. și utilizatorii deja înregistrați nu se vor mai putea conecta. Sigur doriți să continuați?", + "DELETE_SELECTION_TITLE": "Ștergeți IdP", + "DELETE_SELECTION_DESCRIPTION": "Sunteți pe cale să ștergeți un furnizor de identitate. Modificările rezultate sunt irevocabile. Doriți cu adevărat să faceți acest lucru?", + "EMPTY": "Niciun IdP disponibil", + "OIDC": { + "GENERAL": "Informații generale", + "TITLE": "Configurație OIDC", + "DESCRIPTION": "Introduceți datele pentru furnizorul de identitate OIDC." + }, + "JWT": { + "TITLE": "Configurație JWT", + "DESCRIPTION": "Introduceți datele pentru furnizorul de identitate JWT.", + "HEADERNAME": "Nume antet", + "JWTENDPOINT": "Punct final JWT", + "JWTKEYSENDPOINT": "Punct final chei JWT" + }, + "APPLE": { + "TEAMID": "ID echipă", + "KEYID": "ID cheie", + "PRIVATEKEY": "Cheie privată", + "UPDATEPRIVATEKEY": "Actualizați cheia privată", + "UPLOADPRIVATEKEY": "Încărcați cheia privată", + "KEYMAXSIZEEXCEEDED": "Dimensiune maximă de 5 kB depășită." + }, + "SAML": { + "METADATAXML": "XML metadata", + "METADATAURL": "URL metadata", + "BINDING": "Legare", + "SIGNEDREQUEST": "Solicitare semnată", + "NAMEIDFORMAT": "Format ID nume", + "TRANSIENTMAPPINGATTRIBUTENAME": "Nume atribut mapare personalizat", + "TRANSIENTMAPPINGATTRIBUTENAME_DESC": "Nume alternativ de atribut pentru a mapa utilizatorul în cazul în care nameid-format returnat este tranzitoriu, de exemplu, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" + }, + "TOAST": { + "SAVED": "Salvat cu succes.", + "REACTIVATED": "Idp reactivat.", + "DEACTIVATED": "Idp dezactivat.", + "SELECTEDREACTIVATED": "Idp-urile selectate au fost reactivate.", + "SELECTEDDEACTIVATED": "Idp-urile selectate au fost dezactivate.", + "SELECTEDKEYSDELETED": "Idp-urile selectate au fost șterse.", + "DELETED": "Idp eliminat cu succes!", + "ADDED": "Adăugat cu succes.", + "REMOVED": "Eliminat cu succes." + }, + "ISIDTOKENMAPPING": "Mapați din tokenul ID", + "ISIDTOKENMAPPING_DESC": "Dacă este selectat, informațiile despre furnizor sunt mapate din tokenul ID, nu din endpoint-ul userinfo." + }, + "MFA": { + "LIST": { + "MULTIFACTORTITLE": "Fără parolă", + "MULTIFACTORDESCRIPTION": "Definiți-vă multifactorii pentru autentificarea fără parolă aici.", + "SECONDFACTORTITLE": "Autentificare multifactor", + "SECONDFACTORDESCRIPTION": "Definiți alți factori posibili cu care vă puteți securiza autentificarea cu parolă." + }, + "CREATE": { + "TITLE": "Factor nou", + "DESCRIPTION": "Selectați noul dvs. tip de factor." + }, + "DELETE": { + "TITLE": "Ștergeți factorul", + "DESCRIPTION": "Sunteți pe cale să ștergeți un factor din Setările de conectare. Sigur doriți să continuați?" + }, + "TOAST": { + "ADDED": "Adăugat cu succes.", + "SAVED": "Salvat cu succes.", + "DELETED": "Eliminat cu succes" + }, + "TYPE": "Tip", + "MULTIFACTORTYPES": { + "0": "Necunoscut", + "1": "Amprentă, chei de securitate, Face ID și altele" + }, + "SECONDFACTORTYPES": { + "0": "Necunoscut", + "1": "Parolă unică după aplicația de autentificare (TOTP)", + "2": "Amprentă, chei de securitate, Face ID și altele", + "3": "Parolă unică prin e-mail (OTP e-mail)", + "4": "Parolă unică prin SMS (OTP SMS)" + } + }, + "LOGINPOLICY": { + "CREATE": { + "TITLE": "Setări de conectare", + "DESCRIPTION": "Definiți modul în care utilizatorii dvs. pot fi autentificați în organizația dvs." + }, + "IDPS": "Furnizori de identitate", + "ADDIDP": { + "TITLE": "Adăugați furnizor de identate", + "DESCRIPTION": "Puteți selecta furnizori predefiniți sau autocreați pentru autentificare.", + "SELECTIDPS": "Furnizori de identitate" + }, + "PASSWORDLESS": "Conectare fără parolă", + "PASSWORDLESSTYPE": { + "0": "Nepermis", + "1": "Permis" + } + }, + "SMTP": { + "LIST": { + "TITLE": "Furnizor SMTP", + "DESCRIPTION": "Aceștia sunt furnizorii SMTP pentru instanța dvs. ZITADEL. Activați-l pe cel pe care doriți să-l utilizați pentru a trimite notificări utilizatorilor dvs.", + "EMPTY": "Niciun furnizor SMTP disponibil", + "ACTIVATED": "Activat", + "ACTIVATE": "Activați furnizorul", + "DEACTIVATE": "Dezactivați furnizorul", + "TEST": "Testați-vă furnizorul", + "TYPE": "Tip", + "DIALOG": { + "ACTIVATED": "Configurația SMTP a fost activată", + "ACTIVATE_WARN_TITLE": "Activați configurația SMTP", + "ACTIVATE_WARN_DESCRIPTION": "Sunteți pe cale să activați o configurație SMTP. Mai întâi vom dezactiva furnizorul activ curent și apoi vom activa această configurație. Sigur doriți să continuați?", + "DEACTIVATE_WARN_TITLE": "Dezactivați configurația SMTP", + "DEACTIVATE_WARN_DESCRIPTION": "Sunteți pe cale să dezactivați o configurație SMTP. Sigur doriți să continuați?", + "DEACTIVATED": "Configurația SMTP a fost dezactivată", + "DELETE_TITLE": "Ștergeți configurația SMTP", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți o configurație. Confirmați această acțiune tastând numele expeditorului", + "DELETED": "Configurația SMTP a fost ștearsă", + "SENDER": "Tastați {{value}}, pentru a șterge această configurație SMTP.", + "TEST_TITLE": "Testați-vă configurația SMTP", + "TEST_DESCRIPTION": "Specificați o adresă de e-mail pentru a vă testa configurația SMTP pentru acest furnizor", + "TEST_EMAIL": "Adresă de e-mail", + "TEST_RESULT": "Rezultatul testului" + } + }, + "CREATE": { + "TITLE": "Adăugați furnizor SMTP", + "DESCRIPTION": "Selectați unul sau mai mulți dintre următorii furnizori.", + "STEPS": { + "TITLE": "Adăugați furnizor SMTP {{ value }}", + "CREATE_DESC_TITLE": "Introduceți pas cu pas setările dvs. SMTP {{ value }}", + "CURRENT_DESC_TITLE": "Acestea sunt setările dvs. SMTP", + "PROVIDER_SETTINGS": "Setări furnizor SMTP", + "SENDER_SETTINGS": "Setări expeditor", + "NEXT_STEPS": "Pași următori", + "ACTIVATE": { + "TITLE": "Activați-vă furnizorul SMTP", + "DESCRIPTION": "ZITADEL nu poate utiliza acest furnizor SMTP pentru a trimite notificări până când nu îl activați. Dacă activați acest furnizor, orice alt furnizor care era activ va fi acum dezactivat." + }, + "DEACTIVATE": { + "TITLE": "Dezactivați-vă furnizorul SMTP", + "DESCRIPTION": "Dacă dezactivați acest furnizor SMTP, ZITADEL nu îl poate utiliza pentru a trimite notificări până când nu îl activați din nou." + }, + "SAVE_SETTINGS": "Salvați-vă setările", + "TEST": { + "TITLE": "Testați-vă setările", + "DESCRIPTION": "Puteți testa setările furnizorului dvs. SMTP și puteți verifica rezultatul testului înainte de a le salva", + "RESULT": "E-mailul dvs. a fost trimis cu succes" + } + } + }, + "DETAIL": { + "TITLE": "Setări furnizor SMTP" + }, + "EMPTY": "Niciun furnizor SMTP disponibil", + "STEPS": { + "SENDGRID": {} + } + }, + "APP": { + "LIST": "Aplicații", + "COMPLIANCE": "Conformitate OIDC", + "URLS": "Adrese URL", + "CONFIGURATION": "Configurație", + "TOKEN": "Setări token", + "PAGES": { + "TITLE": "Aplicație", + "ID": "ID", + "DESCRIPTION": "Aici puteți edita datele aplicației și configurația acesteia.", + "CREATE": "Creați aplicația", + "CREATE_SELECT_PROJECT": "Selectați mai întâi proiectul dvs.", + "CREATE_NEW_PROJECT": "sau introduceți numele pentru noul dvs. proiect", + "CREATE_DESC_TITLE": "Introduceți pas cu pas detaliile aplicației dvs.", + "CREATE_DESC_SUB": "Va fi generată automat o configurație recomandată.", + "STATE": "Stare", + "DATECREATED": "Creat", + "DATECHANGED": "Modificat", + "URLS": "Adrese URL", + "DELETE": "Ștergeți aplicația", + "JUMPTOPROJECT": "Pentru a configura roluri, autorizații și multe altele, navigați la proiect.", + "DETAIL": { + "TITLE": "Detalii", + "STATE": { + "0": "Nedefinit", + "1": "Activ", + "2": "Inactiv" + } + }, + "DIALOG": { + "CONFIG": { + "TITLE": "Modificați configurația OIDC" + }, + "DELETE": { + "TITLE": "Ștergeți aplicația", + "DESCRIPTION": "Doriți cu adevărat să ștergeți această aplicație?" + } + }, + "NEXTSTEPS": { + "TITLE": "Pași următori", + "0": { + "TITLE": "Adăugați roluri", + "DESC": "Introduceți rolurile proiectului dvs." + }, + "1": { + "TITLE": "Adăugați utilizatori", + "DESC": "Adăugați utilizatori noi ai organizației dvs." + }, + "2": { + "TITLE": "Ajutor și asistență", + "DESC": "Citiți documentația noastră despre crearea aplicațiilor sau contactați asistența noastră" + } + } + }, + "NAMEDIALOG": { + "TITLE": "Redenumiți aplicația", + "DESCRIPTION": "Introduceți noul nume pentru aplicația dvs.", + "NAME": "Nume nou" + }, + "NAME": "Nume", + "TYPE": "Tipul aplicației", + "AUTHMETHOD": "Metoda de autentificare", + "AUTHMETHODSECTION": "Metoda de autentificare", + "GRANT": "Tipuri de grant", + "ADDITIONALORIGINS": "Origini suplimentare", + "ADDITIONALORIGINSDESC": "Dacă doriți să adăugați origini suplimentare la aplicația dvs. care nu sunt utilizate ca redirecționare, puteți face acest lucru aici.", + "ORIGINS": "Origini", + "NOTANORIGIN": "Valoarea introdusă nu este o origine", + "PROSWITCH": "Sunt profesionist. Omiteți acest vrăjitor.", + "NAMEANDTYPESECTION": "Nume și tip", + "TITLEFIRST": "Numele aplicației", + "TYPETITLE": "Tipul de aplicație", + "OIDC": { + "WELLKNOWN": "Puteți prelua linkuri suplimentare de la
punctul final de descoperire.", + "INFO": { + "ISSUER": "Emitent", + "CLIENTID": "ID client" + }, + "CURRENT": "Configurația curentă", + "TOKENSECTIONTITLE": "Opțiuni AuthToken", + "REDIRECTSECTIONTITLE": "Setări de redirecționare", + "REDIRECTTITLE": "Specificați URI-urile unde se va redirecționa conectarea.", + "POSTREDIRECTTITLE": "Acesta este URI-ul de redirecționare după deconectare.", + "REDIRECTDESCRIPTIONWEB": "URI-urile de redirecționare trebuie să înceapă cu https://. http:// este valabil numai cu modul de dezvoltare activat.", + "REDIRECTDESCRIPTIONNATIVE": "URI-urile de redirecționare trebuie să înceapă cu propriul protocol, http://127.0.0.1, http://[::1] sau http://localhost.", + "REDIRECTNOTVALID": "Acest URI de redirecționare nu este valabil.", + "COMMAORENTERSEPERATION": "separați cu ↵", + "TYPEREQUIRED": "Tipul este necesar.", + "TITLE": "Configurație OIDC", + "CLIENTID": "ID client", + "CLIENTSECRET": "Secret client", + "CLIENTSECRET_NOSECRET": "Cu fluxul de autentificare ales, nu este necesar niciun secret și, prin urmare, nu este disponibil.", + "CLIENTSECRET_DESCRIPTION": "Păstrați secretul clientului într-un loc sigur, deoarece va dispărea odată ce dialogul este închis.", + "REGENERATESECRET": "Regenerați secretul clientului", + "DEVMODE": "Mod dezvoltare", + "DEVMODE_ENABLED": "Activat", + "DEVMODE_DISABLED": "Dezactivat", + "DEVMODEDESC": "Atenție: Cu modul de dezvoltare activat, URI-urile de redirecționare nu vor fi validate.", + "SKIPNATIVEAPPSUCCESSPAGE": "Omiteți pagina de succes la conectare", + "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Omiteți pagina de succes după o conectare pentru această aplicație nativă.", + "REDIRECT": "URI-uri de redirecționare", + "REDIRECTSECTION": "URI-uri de redirecționare", + "POSTLOGOUTREDIRECT": "URI-uri de redirecționare după deconectare", + "RESPONSESECTION": "Tipuri de răspuns", + "GRANTSECTION": "Tipuri de grant", + "GRANTTITLE": "Selectați tipurile de grant. Notă: Implicit este disponibil numai pentru aplicațiile bazate pe browser.", + "APPTYPE": { + "0": "Web", + "1": "Agent utilizator", + "2": "Nativ" + }, + "RESPONSETYPE": "Tipuri de răspuns", + "RESPONSE": { + "0": "Cod", + "1": "Token ID", + "2": "Token-Token ID" + }, + "REFRESHTOKEN": "Token de reîmprospătare", + "GRANTTYPE": "Tipuri de grant", + "GRANT": { + "0": "Cod de autorizare", + "1": "Implicit", + "2": "Token de reîmprospătare", + "3": "Cod dispozitiv", + "4": "Schimb token" + }, + "AUTHMETHOD": { + "0": "De bază", + "1": "Post", + "2": "Niciunul", + "3": "JWT cheie privată" + }, + "TOKENTYPE": "Tipul token-ului de autentificare", + "TOKENTYPE0": "Token Bearer", + "TOKENTYPE1": "JWT", + "UNSECUREREDIRECT": "Sper cu siguranță că știți ce faceți.", + "OVERVIEWSECTION": "Prezentare generală", + "OVERVIEWTITLE": "Ați terminat acum. Revizuiți configurația dvs.", + "ACCESSTOKENROLEASSERTION": "Adăugați rolurile utilizatorului la token-ul de acces", + "ACCESSTOKENROLEASSERTION_DESCRIPTION": "Dacă este selectat, rolurile solicitate ale utilizatorului autentificat sunt adăugate la token-ul de acces.", + "IDTOKENROLEASSERTION": "Roluri utilizator în interiorul token-ului ID", + "IDTOKENROLEASSERTION_DESCRIPTION": "Dacă este selectat, rolurile solicitate ale utilizatorului autentificat sunt adăugate la token-ul ID.", + "IDTOKENUSERINFOASSERTION": "Informații utilizator în interiorul token-ului ID", + "IDTOKENUSERINFOASSERTION_DESCRIPTION": "Permite clienților să preia revendicări de profil, e-mail, telefon și adresă din token-ul ID.", + "CLOCKSKEW": "Permite clienților să gestioneze decalajul de ceas al OP și al clientului. Durata (0-5s) va fi adăugată revendicării exp și scăzută din iats, auth_time și nbf.", + "RECOMMENDED": "recomandat", + "NOTRECOMMENDED": "nerecomandat", + "SELECTION": { + "APPTYPE": { + "WEB": { + "TITLE": "Web", + "DESCRIPTION": "Aplicații Web obișnuite precum .net, PHP, Node.js, Java etc." + }, + "NATIVE": { + "TITLE": "Nativ", + "DESCRIPTION": "Aplicații mobile, desktop, dispozitive inteligente etc." + }, + "USERAGENT": { + "TITLE": "Agent utilizator", + "DESCRIPTION": "Aplicații cu o singură pagină (SPA) și în general toate cadrele JS executate în browsere" + } + } + } + }, + "API": { + "INFO": { + "CLIENTID": "ID client" + }, + "REGENERATESECRET": "Regenerați secretul clientului", + "SELECTION": { + "TITLE": "API", + "DESCRIPTION": "API-uri în general" + }, + "AUTHMETHOD": { + "0": "De bază", + "1": "JWT cheie privată" + } + }, + "SAML": { + "SELECTION": { + "TITLE": "SAML", + "DESCRIPTION": "Aplicații SAML" + }, + "CONFIGSECTION": "Configurație SAML", + "CHOOSEMETADATASOURCE": "Furnizați configurația SAML utilizând una dintre următoarele opțiuni:", + "METADATAOPT1": "Opțiunea 1. Specificați adresa URL unde se află fișierul metadata", + "METADATAOPT2": "Opțiunea 2. Încărcați un fișier care conține XML-ul dvs. metadata", + "METADATAOPT3": "Opțiunea 3. Creați un fișier metadata minim din mers furnizând ENTITYID și ACS URL", + "UPLOAD": "Încărcați fișier XML", + "METADATA": "Metadata", + "METADATAFROMFILE": "Metadata din fișier", + "CERTIFICATE": "Certificat SAML", + "DOWNLOADCERT": "Descărcați certificatul SAML", + "CREATEMETADATA": "Creați metadata", + "ENTITYID": "ID entitate", + "ACSURL": "Adresă URL punct final ACS" + }, + "AUTHMETHODS": { + "CODE": { + "TITLE": "Cod", + "DESCRIPTION": "Schimbați codul de autorizare pentru tokenuri" + }, + "PKCE": { + "TITLE": "PKCE", + "DESCRIPTION": "Utilizați un hash aleatoriu în loc de un secret static al clientului pentru mai multă securitate" + }, + "POST": { + "TITLE": "POST", + "DESCRIPTION": "Trimiteți client_id și client_secret ca parte a formularului" + }, + "PK_JWT": { + "TITLE": "JWT cheie privată", + "DESCRIPTION": "Utilizați o cheie privată pentru a vă autoriza aplicația" + }, + "BASIC": { + "TITLE": "De bază", + "DESCRIPTION": "Autentificare cu nume de utilizator și parolă" + }, + "IMPLICIT": { + "TITLE": "Implicit", + "DESCRIPTION": "Obțineți token-urile direct de la punctul final de autorizare" + }, + "DEVICECODE": { + "TITLE": "Cod dispozitiv", + "DESCRIPTION": "Autorizați dispozitivul pe un computer sau smartphone." + }, + "CUSTOM": { + "TITLE": "Personalizat", + "DESCRIPTION": "Setarea dvs. nu corespunde cu nicio altă opțiune." + } + }, + "TOAST": { + "REACTIVATED": "Aplicația a fost reactivată.", + "DEACTIVATED": "Aplicația a fost dezactivată.", + "OIDCUPDATED": "Aplicația a fost actualizată.", + "APIUPDATED": "Aplicația a fost actualizată", + "UPDATED": "Aplicația a fost actualizată.", + "CREATED": "Aplicația a fost creată.", + "CLIENTSECRETREGENERATED": "secretul clientului a fost generat.", + "DELETED": "Aplicația a fost ștearsă.", + "CONFIGCHANGED": "Au fost detectate modificări!" + }, + "LOGINV2": { + "USEV2": "Utilizați noua UI de conectare", + "BASEURL": "Adresă URL de bază personalizată pentru noua UI de conectare" + } + }, + "GENDERS": { + "0": "Necunoscut", + "1": "Femeie", + "2": "Bărbat", + "3": "Altul" + }, + "LANGUAGES": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" + }, + "MEMBER": { + "ADD": "Adăugați un manager", + "CREATIONTYPE": "Tip creare", + "CREATIONTYPES": { + "3": "IAM", + "2": "Organizație", + "0": "Proiect deținut", + "1": "Proiect acordat", + "4": "Proiect" + }, + "EDITROLE": "Editați rolurile", + "EDITFOR": "Editați rolurile pentru utilizator: {{value}}", + "DIALOG": { + "DELETE_TITLE": "Eliminați managerul", + "DELETE_DESCRIPTION": "Sunteți pe cale să eliminați un manager. Sigur doriți să continuați?" + }, + "SHOWDETAILS": "Faceți clic pentru a afișa detalii." + }, + "ROLESLABEL": "Roluri", + "GRANTS": { + "TITLE": "Autorizații", + "DESC": "Acestea sunt toate autorizațiile din organizația dvs.", + "DELETE": "Ștergeți autorizarea", + "EMPTY": "Nicio autorizare găsită", + "ADD": "Creați autorizarea", + "ADD_BTN": "Nou", + "PROJECT": { + "TITLE": "Autorizare", + "DESCRIPTION": "Definiți autorizații pentru proiectul specificat. Rețineți că puteți vedea numai intrările proiectelor și utilizatorilor pentru care aveți permisiunile." + }, + "USER": { + "TITLE": "Autorizare", + "DESCRIPTION": "Definiți autorizații pentru utilizatorul specificat. Rețineți că puteți vedea numai intrările proiectelor și utilizatorilor pentru care aveți permisiunile." + }, + "CREATE": { + "TITLE": "Creați autorizarea", + "DESCRIPTION": "Căutați organizația, proiectul și rolurile corespunzătoare." + }, + "EDIT": { + "TITLE": "Modificați autorizarea" + }, + "DETAIL": { + "TITLE": "Detaliile autorizării", + "DESCRIPTION": "Aici puteți vedea toate detaliile autorizării." + }, + "TOAST": { + "UPDATED": "Autorizarea a fost actualizată.", + "REMOVED": "Autorizarea a fost eliminată", + "BULKREMOVED": "Autorizațiile au fost eliminate.", + "CANTSHOWINFO": "Nu puteți vizita profilul acestui utilizator, deoarece nu sunteți membru al organizației căreia îi aparține acest utilizator" + }, + "DIALOG": { + "DELETE_TITLE": "Ștergeți autorizarea", + "DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți o autorizare. Doriți să continuați?", + "BULK_DELETE_TITLE": "Ștergeți autorizațiile", + "BULK_DELETE_DESCRIPTION": "Sunteți pe cale să ștergeți mai multe autorizații. Doriți să continuați?" + } + }, + "CHANGES": { + "LISTTITLE": "Ultimele modificări", + "BOTTOM": "Ați ajuns la sfârșitul listei.", + "LOADMORE": "Încărcați mai multe", + "ORG": { + "TITLE": "Activitate", + "DESCRIPTION": "Aici puteți vedea ultimele evenimente care au generat o modificare a organizației." + }, + "PROJECT": { + "TITLE": "Activitate", + "DESCRIPTION": "Aici puteți vedea ultimele evenimente care au generat o modificare a proiectului." + }, + "USER": { + "TITLE": "Activitate", + "DESCRIPTION": "Aici puteți vedea ultimele evenimente care au generat o modificare a utilizatorului." + } + } +} diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 8a3ca0c1de..6e2d7df7b4 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -185,6 +185,33 @@ "DESCRIPTION": "Срок действия неактивного токена обновления - это максимальное время, в течение которого токен обновления может оставаться неиспользованным." } }, + "WEB_KEYS": { + "DESCRIPTION": "Управляйте своими OIDC веб-ключами для безопасной подписи и валидации токенов в вашем экземпляре ZITADEL.", + "TABLE": { + "TITLE": "Активные и будущие веб-ключи", + "DESCRIPTION": "Ваши активные и будущие веб-ключи. Активация нового ключа приведёт к деактивации текущего.", + "NOTE": "Примечание: Конечная точка JWKs OIDC возвращает кэшируемый ответ (по умолчанию 5 минут). Избегайте слишком ранней активации ключа, так как он может ещё не быть доступен в кэше и у клиентов.", + "ACTIVATE": "Активировать следующий веб-ключ", + "ACTIVE": "В настоящее время активен", + "NEXT": "Следующий в очереди", + "FUTURE": "Будущий", + "WARNING": "Веб-ключу менее 5 минут" + }, + "CREATE": { + "TITLE": "Создать новый веб-ключ", + "DESCRIPTION": "Создание нового веб-ключа добавит его в ваш список. ZITADEL по умолчанию использует ключи RSA2048 с хешированием SHA256.", + "KEY_TYPE": "Тип ключа", + "BITS": "Биты", + "HASHER": "Алгоритм хеширования", + "CURVE": "Кривая" + }, + "PREVIOUS_TABLE": { + "TITLE": "Предыдущие веб-ключи", + "DESCRIPTION": "Это ваши предыдущие веб-ключи, которые больше не активны.", + "DEACTIVATED_ON": "Деактивирован" + } + }, + "MESSAGE_TEXTS": { "TITLE": "Тексты сообщений", "DESCRIPTION": "Настройте тексты ваших уведомлений по электронной почте или SMS. Если вы хотите отключить некоторые языки, ограничьте их в настройках языка ваших экземпляров.", @@ -501,6 +528,115 @@ "DOWNLOAD": "Скачать", "APPLY": "Применять" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Действия", + "DESCRIPTION": "Действия позволяют запускать пользовательский код в ответ на API-запросы, события или определенные функции. Используйте их для расширения Zitadel, автоматизации рабочих процессов и интеграции с другими системами.", + "TYPES": { + "request": "Запрос", + "response": "Ответ", + "events": "События", + "function": "Функция" + }, + "DIALOG": { + "CREATE_TITLE": "Создать действие", + "UPDATE_TITLE": "Обновить действие", + "TYPE": { + "DESCRIPTION": "Выберите, когда вы хотите запустить это действие", + "REQUEST": { + "TITLE": "Запрос", + "DESCRIPTION": "Запросы, которые происходят внутри Zitadel. Это может быть что-то вроде вызова запроса на вход." + }, + "RESPONSE": { + "TITLE": "Ответ", + "DESCRIPTION": "Ответ на запрос внутри Zitadel. Подумайте об ответе, который вы получаете при получении пользователя." + }, + "EVENTS": { + "TITLE": "События", + "DESCRIPTION": "События, которые происходят внутри Zitadel. Это может быть что угодно, например, создание пользователем учетной записи, успешный вход и т. д." + }, + "FUNCTIONS": { + "TITLE": "Функции", + "DESCRIPTION": "Функции, которые вы можете вызвать внутри Zitadel. Это может быть что угодно, от отправки электронной почты до создания пользователя." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Выберите, применяется ли это действие ко всем запросам, к определенной службе (например, управление пользователями) или к одному запросу (например, создать пользователя).", + "ALL": { + "TITLE": "Все", + "DESCRIPTION": "Выберите это, если вы хотите запустить свое действие при каждом запросе" + }, + "SELECT_SERVICE": { + "TITLE": "Выбрать службу", + "DESCRIPTION": "Выберите службу Zitadel для вашего действия." + }, + "SELECT_METHOD": { + "TITLE": "Выбрать метод", + "DESCRIPTION": "Если вы хотите запустить только для определенного запроса, выберите его здесь", + "NOTE": "Если вы не выберете метод, ваше действие будет запускаться при каждом запросе в выбранной вами службе." + }, + "FUNCTIONNAME": { + "TITLE": "Имя функции", + "DESCRIPTION": "Выберите функцию, которую вы хотите запустить" + }, + "SELECT_GROUP": { + "TITLE": "Установить группу", + "DESCRIPTION": "Если вы хотите запустить только для группы событий, установите группу здесь" + }, + "SELECT_EVENT": { + "TITLE": "Выбрать событие", + "DESCRIPTION": "Если вы хотите запустить только для определенного события, укажите его здесь" + } + }, + "TARGET": { + "DESCRIPTION": "Вы можете выбрать запуск цели или запустить ее в тех же условиях, что и другие цели.", + "TARGET": { + "DESCRIPTION": "Цель, которую вы хотите запустить для этого действия" + }, + "CONDITIONS": { + "DESCRIPTION": "Условия выполнения" + } + } + }, + "TABLE": { + "CONDITION": "Условие", + "TYPE": "Тип", + "TARGET": "Цель", + "CREATIONDATE": "Дата создания" + } + }, + "TARGET": { + "TITLE": "Цели", + "DESCRIPTION": "Цель — это место назначения кода, который вы хотите запустить из действия. Создайте цель здесь и добавьте ее к своим действиям.", + "CREATE": { + "TITLE": "Создать свою цель", + "DESCRIPTION": "Создайте свою собственную цель за пределами Zitadel", + "NAME": "Имя", + "NAME_DESCRIPTION": "Дайте своей цели четкое, описательное имя, чтобы ее было легко идентифицировать позже", + "TYPE": "Тип", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Вызов", + "restAsync": "REST Асинхронный" + }, + "ENDPOINT": "Конечная точка", + "ENDPOINT_DESCRIPTION": "Введите конечную точку, где размещен ваш код. Убедитесь, что он доступен для нас!", + "TIMEOUT": "Тайм-аут", + "TIMEOUT_DESCRIPTION": "Установите максимальное время, в течение которого ваша цель должна ответить. Если это займет больше времени, мы остановим запрос.", + "INTERRUPT_ON_ERROR": "Прервать при ошибке", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Остановите все выполнения, когда цели вернут ошибку", + "INTERRUPT_ON_ERROR_WARNING": "Внимание: опция «Прервать при ошибке» останавливает выполнение при сбое, что может привести к блокировке. Протестируйте с отключённой опцией, чтобы избежать блокировки входа/создания.", + "AWAIT_RESPONSE": "Ожидать ответа", + "AWAIT_RESPONSE_DESCRIPTION": "Мы подождем ответа, прежде чем делать что-либо еще. Полезно, если вы планируете использовать несколько целей для одного действия" + }, + "TABLE": { + "NAME": "Имя", + "ENDPOINT": "Конечная точка", + "CREATIONDATE": "Дата создания", + "REORDER": "Изменить порядок" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Имеет контроль над всем экземпляром, включая все организации", "IAM_OWNER_VIEWER": "Имеет разрешение на просмотр всего экземпляра, включая все организации", @@ -508,14 +644,17 @@ "IAM_USER_MANAGER": "Имеет разрешение на создание и управление пользователями", "IAM_ADMIN_IMPERSONATOR": "Имеет разрешение выдавать себя за администратора и конечных пользователей из всех организаций", "IAM_END_USER_IMPERSONATOR": "Имеет разрешение выдавать себя за конечных пользователей из всех организаций", + "IAM_LOGIN_CLIENT": "Имеет разрешение на управление клиентами входа", "ORG_OWNER": "Имеет разрешение на всю организацию", "ORG_USER_MANAGER": "Имеет разрешение на создание и управление пользователями организации", "ORG_OWNER_VIEWER": "Имеет разрешение на просмотр всей организации", + "ORG_SETTINGS_MANAGER": "Имеет разрешение на управление настройками организации", "ORG_USER_PERMISSION_EDITOR": "Имеет разрешение на управление допусками пользователей", "ORG_PROJECT_PERMISSION_EDITOR": "Имеет разрешение на управление допусками проекта", "ORG_PROJECT_CREATOR": "Имеет разрешение на создание собственных проектов и базовых настроек", "ORG_ADMIN_IMPERSONATOR": "Имеет разрешение выдавать себя за администратора и конечных пользователей организации", "ORG_END_USER_IMPERSONATOR": "Имеет разрешение выдавать себя за конечных пользователей организации", + "ORG_USER_SELF_MANAGER": "Имеет разрешение на управление своим собственным пользователем", "PROJECT_OWNER": "Имеет разрешение на весь проект", "PROJECT_OWNER_VIEWER": "Имеет разрешение на просмотр всего проекта", "PROJECT_OWNER_GLOBAL": "Имеет разрешение на весь проект", @@ -794,7 +933,10 @@ "PHONESECTION": "Номера телефонов", "PASSWORDSECTION": "Начальный пароль", "ADDRESSANDPHONESECTION": "Номер телефона", - "INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных." + "INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных.", + "SETUPAUTHENTICATIONLATER": "Настроить аутентификацию позже для этого пользователя.", + "INVITATION": "Отправить приглашение по электронной почте для настройки аутентификации и подтверждения электронной почты.", + "INITIALPASSWORD": "Установите начальный пароль для пользователя." }, "CODEDIALOG": { "TITLE": "Подтвердить номер телефона", @@ -816,6 +958,9 @@ "EMAIL": "Электронная почта", "PHONE": "Номер телефона", "PHONE_HINT": "Используйте 00 или символ +, за которым следует код страны вызываемого абонента, или выберите страну из раскрывающегося списка и введите номер телефона.", + "PHONE_VERIFIED": "Номер телефона подтвержден", + "SEND_SMS": "Отправить проверочный SMS", + "SEND_EMAIL": "Отправить e-mail", "USERNAME": "Имя пользователя", "CHANGEUSERNAME": "Изменить", "CHANGEUSERNAME_TITLE": "Изменить имя пользователя", @@ -967,6 +1112,14 @@ "5": "Приостановлен", "6": "Начальный" }, + "STATEV2": { + "0": "Неизвестен", + "1": "Активен", + "2": "Неактивен", + "3": "Удалён", + "4": "Заблокирован", + "5": "Начальный" + }, "SEARCH": { "ADDITIONAL": "Логин (текущая организация)", "ADDITIONAL-EXTERNAL": "Логин (внешняя организация)" @@ -1384,6 +1537,7 @@ "BRANDING": "Брендинг", "PRIVACYPOLICY": "Политика конфиденциальности", "OIDC": "Срок действия токена OIDC", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Отображение ключа", "SECURITY": "Настройки безопасности", "EVENTS": "События", @@ -1429,7 +1583,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1528,6 +1683,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 позволяют управлять выполнением данных и целевыми объектами. Если флаг включен, вы сможете использовать новый API и его функции.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Окончание сеанса", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Если флаг включен, вы сможете завершить отдельный сеанс из интерфейса пользователя входа, предоставив id_token с претензией `sid` в качестве id_token_hint на конечной точке end_session. Обратите внимание, что в настоящее время все сеансы одного и того же пользовательского агента (браузера) завершаются в интерфейсе пользователя входа. Сеансы, управляемые через API сеанса, уже позволяют завершать отдельные сеансы.", + "DEBUGOIDCPARENTERROR": "Отладка Ошибки Родителя OIDC", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Если флаг включен, ошибка родителя OIDC будет записана в консоль.", + "DISABLEUSERTOKENEVENT": "Отключить Событие Токена Пользователя", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Включить Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout реализует OpenID Connect Back-Channel Logout 1.0 и может использоваться для уведомления клиентов о завершении сеанса у поставщика OpenID.", + "PERMISSIONCHECKV2": "Проверка Разрешений V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Если флаг включен, вы сможете использовать новый API и его функции.", + "WEBKEY": "Веб-ключ", + "WEBKEY_DESCRIPTION": "Если флаг включен, вы сможете использовать новый API и его функции.", "STATES": { "INHERITED": "Наследовать", "ENABLED": "Включено", @@ -1538,7 +1703,12 @@ "ENABLED": "«Включено» наследуется", "DISABLED": "«Выключено» передается по наследству" }, - "RESET": "Установить все по умолчанию" + "RESET": "Установить все по умолчанию", + "CONSOLEUSEV2USERAPI": "Используйте V2 API в консоли для создания пользователей", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Когда этот флаг включен, консоль использует V2 User API для создания новых пользователей. С API V2 новые пользователи создаются без начального состояния.", + "LOGINV2": "Вход V2", + "LOGINV2_DESCRIPTION": "Включение этой опции активирует новый интерфейс входа на основе TypeScript с улучшенной безопасностью, производительностью и возможностью настройки.", + "LOGINV2_BASEURI": "Базовый URI" }, "DIALOG": { "RESET": { @@ -1679,7 +1849,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "LOCALE": "Код языка", "LOCALES": { @@ -2303,7 +2475,9 @@ "REMOVED": "Удалено успешно." }, "ISIDTOKENMAPPING": "Карта из ID-токена", - "ISIDTOKENMAPPING_DESC": "Если этот флажок установлен, информация о поставщике сопоставляется с маркером идентификатора, а не с конечной точкой информации о пользователе." + "ISIDTOKENMAPPING_DESC": "Если этот флажок установлен, информация о поставщике сопоставляется с маркером идентификатора, а не с конечной точкой информации о пользователе.", + "USEPKCE": "Используйте ПКСЕ", + "USEPKCE_DESC": "Определяет, включены ли параметры code_challenge и code_challenge_method в запрос аутентификации." }, "MFA": { "LIST": { @@ -2678,7 +2852,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Добавить менеджера", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index fd698478fb..e747571f7a 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -185,6 +185,32 @@ "DESCRIPTION": "Den inaktiva förnyelsetokenens livslängd är den maximala tiden en förnyelsetoken kan vara oanvänd." } }, + "WEB_KEYS": { + "DESCRIPTION": "Hantera dina OIDC-webbnycklar för att säkert signera och validera tokens för din ZITADEL-instans.", + "TABLE": { + "TITLE": "Aktiva och framtida webbnycklar", + "DESCRIPTION": "Dina aktiva och kommande webbnycklar. Aktivering av en ny nyckel kommer att inaktivera den nuvarande.", + "NOTE": "Observera: JWKs OIDC-slutpunkten returnerar ett cachebart svar (standard 5 min). Undvik att aktivera en nyckel för tidigt, eftersom den kanske ännu inte är tillgänglig i cache och för klienter.", + "ACTIVATE": "Aktivera nästa webbnyckel", + "ACTIVE": "För närvarande aktiv", + "NEXT": "Nästa i kön", + "FUTURE": "Framtida", + "WARNING": "Webbnyckeln är mindre än 5 minuter gammal" + }, + "CREATE": { + "TITLE": "Skapa ny webbnyckel", + "DESCRIPTION": "Att skapa en ny webbnyckel lägger till den i din lista. ZITADEL använder som standard RSA2048-nycklar med en SHA256-hasher.", + "KEY_TYPE": "Nyckeltyp", + "BITS": "Bitar", + "HASHER": "Hasher", + "CURVE": "Kurva" + }, + "PREVIOUS_TABLE": { + "TITLE": "Tidigare webbnycklar", + "DESCRIPTION": "Detta är dina tidigare webbnycklar som inte längre är aktiva.", + "DEACTIVATED_ON": "Inaktiverad den" + } + }, "MESSAGE_TEXTS": { "TITLE": "Meddelandetexter", "DESCRIPTION": "Anpassa texterna i dina notifikationsmail eller SMS-meddelanden. Om du vill inaktivera några av språken, begränsa dem i dina instansers språkinställningar.", @@ -502,6 +528,115 @@ "DOWNLOAD": "Ladda ner", "APPLY": "Tillämpa" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "Åtgärder", + "DESCRIPTION": "Åtgärder låter dig köra anpassad kod som svar på API-förfrågningar, händelser eller specifika funktioner. Använd dem för att utöka Zitadel, automatisera arbetsflöden och integrera med andra system.", + "TYPES": { + "request": "Förfrågan", + "response": "Svar", + "events": "Händelser", + "function": "Funktion" + }, + "DIALOG": { + "CREATE_TITLE": "Skapa en åtgärd", + "UPDATE_TITLE": "Uppdatera en åtgärd", + "TYPE": { + "DESCRIPTION": "Välj när du vill att denna åtgärd ska köras", + "REQUEST": { + "TITLE": "Förfrågan", + "DESCRIPTION": "Förfrågningar som sker inom Zitadel. Detta kan vara något som ett inloggningsförfrågningsanrop." + }, + "RESPONSE": { + "TITLE": "Svar", + "DESCRIPTION": "Ett svar från en förfrågan inom Zitadel. Tänk på svaret du får tillbaka från att hämta en användare." + }, + "EVENTS": { + "TITLE": "Händelser", + "DESCRIPTION": "Händelser som händer inom Zitadel. Detta kan vara vad som helst som en användare som skapar ett konto, en lyckad inloggning etc." + }, + "FUNCTIONS": { + "TITLE": "Funktioner", + "DESCRIPTION": "Funktioner som du kan anropa inom Zitadel. Detta kan vara allt från att skicka ett e-postmeddelande till att skapa en användare." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Välj om denna åtgärd gäller för alla förfrågningar, en specifik tjänst (t.ex. användarhantering) eller en enskild förfrågan (t.ex. skapa användare).", + "ALL": { + "TITLE": "Alla", + "DESCRIPTION": "Välj detta om du vill köra din åtgärd på varje förfrågan" + }, + "SELECT_SERVICE": { + "TITLE": "Välj tjänst", + "DESCRIPTION": "Välj en Zitadel-tjänst för din åtgärd." + }, + "SELECT_METHOD": { + "TITLE": "Välj metod", + "DESCRIPTION": "Om du bara vill köra på en specifik förfrågan, välj den här", + "NOTE": "Om du inte väljer en metod körs din åtgärd på varje förfrågan i din valda tjänst." + }, + "FUNCTIONNAME": { + "TITLE": "Funktionsnamn", + "DESCRIPTION": "Välj den funktion du vill köra" + }, + "SELECT_GROUP": { + "TITLE": "Ange grupp", + "DESCRIPTION": "Om du bara vill köra på en grupp händelser, ange gruppen här" + }, + "SELECT_EVENT": { + "TITLE": "Välj händelse", + "DESCRIPTION": "Om du bara vill köra på en specifik händelse, ange den här" + } + }, + "TARGET": { + "DESCRIPTION": "Du kan välja att köra ett mål eller att köra det under samma villkor som andra mål.", + "TARGET": { + "DESCRIPTION": "Målet du vill köra för denna åtgärd" + }, + "CONDITIONS": { + "DESCRIPTION": "Körningsvillkor" + } + } + }, + "TABLE": { + "CONDITION": "Villkor", + "TYPE": "Typ", + "TARGET": "Mål", + "CREATIONDATE": "Skapat datum" + } + }, + "TARGET": { + "TITLE": "Mål", + "DESCRIPTION": "Ett mål är destinationen för koden du vill köra från en åtgärd. Skapa ett mål här och lägg till det i dina åtgärder.", + "CREATE": { + "TITLE": "Skapa ditt mål", + "DESCRIPTION": "Skapa ditt eget mål utanför Zitadel", + "NAME": "Namn", + "NAME_DESCRIPTION": "Ge ditt mål ett tydligt, beskrivande namn för att göra det enkelt att identifiera senare", + "TYPE": "Typ", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Anrop", + "restAsync": "REST Asynkron" + }, + "ENDPOINT": "Slutpunkt", + "ENDPOINT_DESCRIPTION": "Ange slutpunkten där din kod finns. Se till att den är tillgänglig för oss!", + "TIMEOUT": "Tidsgräns", + "TIMEOUT_DESCRIPTION": "Ange den maximala tid ditt mål har att svara. Om det tar längre tid stoppar vi förfrågan.", + "INTERRUPT_ON_ERROR": "Avbryt vid fel", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Stoppa alla körningar när målen returnerar ett fel", + "INTERRUPT_ON_ERROR_WARNING": "Varning: ”Avbryt vid fel” stoppar åtgärder vid fel och kan leda till att du blir utelåst. Testa med funktionen avstängd för att undvika att blockera inloggning/skapa.", + "AWAIT_RESPONSE": "Vänta på svar", + "AWAIT_RESPONSE_DESCRIPTION": "Vi väntar på ett svar innan vi gör något annat. Användbart om du avser att använda flera mål för en enda åtgärd" + }, + "TABLE": { + "NAME": "Namn", + "ENDPOINT": "Slutpunkt", + "CREATIONDATE": "Skapat datum", + "REORDER": "Ordna om" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "Har kontroll över hela instansen, inklusive alla organisationer", "IAM_OWNER_VIEWER": "Har behörighet att granska hela instansen, inklusive alla organisationer", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "Har behörighet att skapa och hantera användare", "IAM_ADMIN_IMPERSONATOR": "Har behörighet att imitera administratörer och slutanvändare från alla organisationer", "IAM_END_USER_IMPERSONATOR": "Har behörighet att imitera slutanvändare från alla organisationer", + "IAM_LOGIN_CLIENT": "Har behörighet att hantera inloggningsklienter", "ORG_OWNER": "Har behörighet över hela organisationen", "ORG_USER_MANAGER": "Har behörighet att skapa och hantera användare i organisationen", "ORG_OWNER_VIEWER": "Har behörighet att granska hela organisationen", + "ORG_SETTINGS_MANAGER": "Har behörighet att hantera organisationens inställningar", "ORG_USER_PERMISSION_EDITOR": "Har behörighet att hantera användarbehörigheter", "ORG_PROJECT_PERMISSION_EDITOR": "Har behörighet att hantera projektbehörigheter", "ORG_PROJECT_CREATOR": "Har behörighet att skapa egna projekt och underliggande inställningar", "ORG_ADMIN_IMPERSONATOR": "Har behörighet att imitera administratörer och slutanvändare från organisationen", "ORG_END_USER_IMPERSONATOR": "Har behörighet att imitera slutanvändare från organisationen", + "ORG_USER_SELF_MANAGER": "Har behörighet att hantera sin egen användare", "PROJECT_OWNER": "Har behörighet över hela projektet", "PROJECT_OWNER_VIEWER": "Har behörighet att granska hela projektet", "PROJECT_OWNER_GLOBAL": "Har behörighet över hela projektet", @@ -787,7 +925,10 @@ "PHONESECTION": "Telefonnummer", "PASSWORDSECTION": "Initialt lösenord", "ADDRESSANDPHONESECTION": "Telefonnummer", - "INITMAILDESCRIPTION": "Om båda alternativen är valda kommer inget e-postmeddelande för initialisering att skickas. Om endast ett av alternativen är valt kommer ett e-postmeddelande för att tillhandahålla/verifiera uppgifterna att skickas." + "INITMAILDESCRIPTION": "Om båda alternativen är valda kommer inget e-postmeddelande för initialisering att skickas. Om endast ett av alternativen är valt kommer ett e-postmeddelande för att tillhandahålla/verifiera uppgifterna att skickas.", + "SETUPAUTHENTICATIONLATER": "Ställ in autentisering senare för den här användaren.", + "INVITATION": "Skicka en inbjudningsmail för autentiseringsinställning och e-postverifiering.", + "INITIALPASSWORD": "Ställ in ett initialt lösenord för användaren." }, "CODEDIALOG": { "TITLE": "Verifiera telefonnummer", @@ -809,6 +950,9 @@ "EMAIL": "E-post", "PHONE": "Telefonnummer", "PHONE_HINT": "Använd + symbolen följt av landskoden, eller välj landet från rullgardinsmenyn och ange sedan telefonnumret", + "PHONE_VERIFIED": "Telefonnummer verifierat", + "SEND_SMS": "Skicka verifierings-SMS", + "SEND_EMAIL": "Skicka E-post", "USERNAME": "Användarnamn", "CHANGEUSERNAME": "ändra", "CHANGEUSERNAME_TITLE": "Ändra användarnamn", @@ -949,6 +1093,14 @@ "5": "Suspenderad", "6": "Initial" }, + "STATEV2": { + "0": "Okänd", + "1": "Aktiv", + "2": "Inaktiv", + "3": "Raderad", + "4": "Låst", + "5": "Initial" + }, "SEARCH": { "ADDITIONAL": "Inloggningsnamn (nuvarande organisation)", "ADDITIONAL-EXTERNAL": "Inloggningsnamn (extern organisation)" @@ -1344,6 +1496,7 @@ "BRANDING": "Varumärke", "PRIVACYPOLICY": "Externa länkar", "OIDC": "OIDC-token livstid och utgång", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "Hemlighetsgenerator", "SECURITY": "Säkerhetsinställningar", "EVENTS": "Händelser", @@ -1389,7 +1542,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1480,6 +1634,16 @@ "ACTIONS_DESCRIPTION": "Åtgärder v2 tillåter att hantera dataexekveringar och mål. Om flaggan är aktiverad kommer du att kunna använda det nya API:et och dess funktioner.", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Session avslutning", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Om flaggan är aktiverad, kan du avsluta en enskild session från inloggningsgränssnittet genom att ange en id_token med ett `sid`-krav som id_token_hint på slutpunkten end_session. Observera att för närvarande alla sessioner från samma användaragent (webbläsare) avslutas i inloggningsgränssnittet. Sessioner som hanteras via Session API tillåter redan avslutning av enskilda sessioner.", + "DEBUGOIDCPARENTERROR": "Debugga OIDC Föräldrafel", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Om flaggan är aktiverad kommer OIDC-föräldrafel att loggas i konsolen.", + "DISABLEUSERTOKENEVENT": "Inaktivera Användartokenhändelse", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Aktivera Backchannel Logout", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementerar OpenID Connect Back-Channel Logout 1.0 och kan användas för att meddela klienter om sessionens avslutning hos OpenID-leverantören.", + "PERMISSIONCHECKV2": "Behörighetskontroll V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Om flaggan är aktiverad kan du använda den nya API:n och dess funktioner.", + "WEBKEY": "Webbnyckel", + "WEBKEY_DESCRIPTION": "Om flaggan är aktiverad kan du använda den nya API:n och dess funktioner.", "STATES": { "INHERITED": "Ärv", "ENABLED": "Aktiverad", @@ -1490,7 +1654,12 @@ "ENABLED": "\"Aktiverad\" ärvs", "DISABLED": "\"Inaktiverad\" ärvs" }, - "RESET": "Återställ allt till arv" + "RESET": "Återställ allt till arv", + "CONSOLEUSEV2USERAPI": "Använd V2 API i konsolen för att skapa användare", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "När denna flagga är aktiverad använder konsolen V2 User API för att skapa nya användare. Med V2 API startar nyligen skapade användare utan ett initialt tillstånd.", + "LOGINV2": "Inloggning V2", + "LOGINV2_DESCRIPTION": "Att aktivera detta startar det nya inloggningsgränssnittet baserat på TypeScript med förbättrad säkerhet, prestanda och anpassning.", + "LOGINV2_BASEURI": "Bas-URI" }, "DIALOG": { "RESET": { @@ -1627,7 +1796,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "E-postverifiering klar", @@ -2216,7 +2387,9 @@ "REMOVED": "Borttagen framgångsrikt." }, "ISIDTOKENMAPPING": "Mappa från ID-token", - "ISIDTOKENMAPPING_DESC": "Om valt, mappas leverantörsinformation från ID-token, inte från användarinfo-slutpunkten." + "ISIDTOKENMAPPING_DESC": "Om valt, mappas leverantörsinformation från ID-token, inte från användarinfo-slutpunkten.", + "USEPKCE": "Använd PKCE", + "USEPKCE_DESC": "Avgör om parametrarna code_challenge och code_challenge_method ingår i autentiseringsbegäran" }, "MFA": { "LIST": { @@ -2599,7 +2772,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "Lägg till en administratör", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 99bf053225..945e4200ef 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -185,6 +185,32 @@ "DESCRIPTION": "空闲刷新令牌的生命周期是刷新令牌可以未使用的最长时间。" } }, + "WEB_KEYS": { + "DESCRIPTION": "管理您的 OIDC Web 密钥,以安全地签署和验证您的 ZITADEL 实例的令牌。", + "TABLE": { + "TITLE": "活动和未来的 Web 密钥", + "DESCRIPTION": "您的当前活动和即将启用的 Web 密钥。激活新密钥将会停用当前密钥。", + "NOTE": "注意:JWKs OIDC 端点返回可缓存的响应(默认 5 分钟)。请避免过早激活密钥,否则它可能尚未在缓存或客户端中可用。", + "ACTIVATE": "激活下一个 Web 密钥", + "ACTIVE": "当前活动", + "NEXT": "队列中的下一个", + "FUTURE": "未来", + "WARNING": "Web密钥不到5分钟。" + }, + "CREATE": { + "TITLE": "创建新的 Web 密钥", + "DESCRIPTION": "创建新的 Web 密钥会将其添加到您的列表。ZITADEL 默认使用 RSA2048 密钥和 SHA256 哈希算法。", + "KEY_TYPE": "密钥类型", + "BITS": "位数", + "HASHER": "哈希算法", + "CURVE": "曲线" + }, + "PREVIOUS_TABLE": { + "TITLE": "先前的 Web 密钥", + "DESCRIPTION": "这些是您之前使用但不再活动的 Web 密钥。", + "DEACTIVATED_ON": "停用时间" + } + }, "MESSAGE_TEXTS": { "TITLE": "消息文本", "DESCRIPTION": "自定义您的通知电子邮件或短信消息的文本。如果您想禁用某些语言,请在您的实例语言设置中限制它们。", @@ -502,6 +528,115 @@ "DOWNLOAD": "下载", "APPLY": "申请" }, + "ACTIONSTWO": { + "EXECUTION": { + "TITLE": "操作", + "DESCRIPTION": "操作允许您运行自定义代码以响应 API 请求、事件或特定函数。使用它们来扩展 Zitadel、自动化工作流程并与其他系统集成。", + "TYPES": { + "request": "请求", + "response": "响应", + "events": "事件", + "function": "函数" + }, + "DIALOG": { + "CREATE_TITLE": "创建操作", + "UPDATE_TITLE": "更新操作", + "TYPE": { + "DESCRIPTION": "选择您希望此操作运行的时间", + "REQUEST": { + "TITLE": "请求", + "DESCRIPTION": "Zitadel 中发生的请求。这可能是登录请求调用之类的操作。" + }, + "RESPONSE": { + "TITLE": "响应", + "DESCRIPTION": "来自 Zitadel 中请求的响应。考虑一下从获取用户返回的响应。" + }, + "EVENTS": { + "TITLE": "事件", + "DESCRIPTION": "Zitadel 中发生的事件。这可能是用户创建帐户、成功登录等任何操作。" + }, + "FUNCTIONS": { + "TITLE": "函数", + "DESCRIPTION": "您可以在 Zitadel 中调用的函数。这可能是从发送电子邮件到创建用户的任何操作。" + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "选择此操作是适用于所有请求、特定服务(例如用户管理)还是单个请求(例如创建用户)。", + "ALL": { + "TITLE": "全部", + "DESCRIPTION": "如果您希望在每个请求上运行您的操作,请选择此项" + }, + "SELECT_SERVICE": { + "TITLE": "选择服务", + "DESCRIPTION": "为您的操作选择一个 Zitadel 服务。" + }, + "SELECT_METHOD": { + "TITLE": "选择方法", + "DESCRIPTION": "如果您只想在特定请求上执行,请在此处选择它", + "NOTE": "如果您不选择方法,您的操作将在您选择的服务中的每个请求上运行。" + }, + "FUNCTIONNAME": { + "TITLE": "函数名称", + "DESCRIPTION": "选择您要执行的函数" + }, + "SELECT_GROUP": { + "TITLE": "设置组", + "DESCRIPTION": "如果您只想在事件组上执行,请在此处设置组" + }, + "SELECT_EVENT": { + "TITLE": "选择事件", + "DESCRIPTION": "如果您只想在特定事件上执行,请在此处指定它" + } + }, + "TARGET": { + "DESCRIPTION": "您可以选择执行目标,或在与其他目标相同的条件下运行它。", + "TARGET": { + "DESCRIPTION": "您要为此操作执行的目标" + }, + "CONDITIONS": { + "DESCRIPTION": "执行条件" + } + } + }, + "TABLE": { + "CONDITION": "条件", + "TYPE": "类型", + "TARGET": "目标", + "CREATIONDATE": "创建日期" + } + }, + "TARGET": { + "TITLE": "目标", + "DESCRIPTION": "目标是您要从操作中执行的代码的目标。在此处创建一个目标并将其添加到您的操作中。", + "CREATE": { + "TITLE": "创建您的目标", + "DESCRIPTION": "在 Zitadel 外部创建您自己的目标", + "NAME": "名称", + "NAME_DESCRIPTION": "为您的目标提供清晰、描述性的名称,以便稍后轻松识别", + "TYPE": "类型", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST 调用", + "restAsync": "REST 异步" + }, + "ENDPOINT": "端点", + "ENDPOINT_DESCRIPTION": "输入您的代码托管的端点。确保我们可以访问它!", + "TIMEOUT": "超时", + "TIMEOUT_DESCRIPTION": "设置您的目标必须响应的最大时间。如果花费的时间更长,我们将停止请求。", + "INTERRUPT_ON_ERROR": "错误时中断", + "INTERRUPT_ON_ERROR_DESCRIPTION": "当目标返回错误时,停止所有执行", + "INTERRUPT_ON_ERROR_WARNING": "注意:“出错时中断”会在失败时停止操作,存在被锁定的风险。请在禁用该选项的情况下进行测试,以避免阻止登录/创建。", + "AWAIT_RESPONSE": "等待响应", + "AWAIT_RESPONSE_DESCRIPTION": "我们将在执行任何其他操作之前等待响应。如果您打算为单个操作使用多个目标,这将非常有用" + }, + "TABLE": { + "NAME": "名称", + "ENDPOINT": "端点", + "CREATIONDATE": "创建日期", + "REORDER": "重新排序" + } + } + }, "MEMBERROLES": { "IAM_OWNER": "控制整个实例,包括所有组织", "IAM_OWNER_VIEWER": "有权审查整个实例,包括所有组织", @@ -509,14 +644,17 @@ "IAM_USER_MANAGER": "有权创建和管理用户", "IAM_ADMIN_IMPERSONATOR": "有权模拟所有组织的管理员和最终用户", "IAM_END_USER_IMPERSONATOR": "有权模拟所有组织的最终用户", + "IAM_LOGIN_CLIENT": "具有管理登录客户端的权限", "ORG_OWNER": "拥有整个组织的权限", "ORG_USER_MANAGER": "有权创建和管理组织的用户", "ORG_OWNER_VIEWER": "有权审查整个组织", + "ORG_SETTINGS_MANAGER": "有权管理组织的设置", "ORG_USER_PERMISSION_EDITOR": "有权管理用户授权", "ORG_PROJECT_PERMISSION_EDITOR": "有权管理项目授权", "ORG_PROJECT_CREATOR": "有权创建自己的项目和基础设置", "ORG_ADMIN_IMPERSONATOR": "有权模拟组织的管理员和最终用户", "ORG_END_USER_IMPERSONATOR": "有权模拟组织的最终用户", + "ORG_USER_SELF_MANAGER": "有权管理自己的用户", "PROJECT_OWNER": "拥有整个项目的权限", "PROJECT_OWNER_VIEWER": "有权审查整个项目", "PROJECT_OWNER_GLOBAL": "拥有整个项目的权限", @@ -787,7 +925,10 @@ "PHONESECTION": "手机号码", "PASSWORDSECTION": "初始密码", "ADDRESSANDPHONESECTION": "手机号码", - "INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。" + "INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。", + "SETUPAUTHENTICATIONLATER": "稍后为此用户设置身份验证。", + "INVITATION": "发送邀请邮件以进行身份验证设置和电子邮件验证。", + "INITIALPASSWORD": "为用户设置初始密码。" }, "CODEDIALOG": { "TITLE": "验证手机号码", @@ -809,6 +950,9 @@ "EMAIL": "电子邮件", "PHONE": "手机号码", "PHONE_HINT": "使用+号,后跟呼叫者的国家/地区代码,或从下拉列表中选择国家/地区,最后输入电话号码", + "PHONE_VERIFIED": "电话号码已验证", + "SEND_SMS": "发送验证短信", + "SEND_EMAIL": "发送电子邮件", "USERNAME": "用户名", "CHANGEUSERNAME": "修改", "CHANGEUSERNAME_TITLE": "修改用户名称", @@ -949,6 +1093,14 @@ "5": "已暂停", "6": "初始化" }, + "STATEV2": { + "0": "未知", + "1": "启用", + "2": "停用", + "3": "已删除", + "4": "已锁定", + "5": "初始化" + }, "SEARCH": { "ADDITIONAL": "登录名 (当前组织)", "ADDITIONAL-EXTERNAL": "登录名 (外部组织)" @@ -1340,6 +1492,7 @@ "BRANDING": "品牌标识", "PRIVACYPOLICY": "隐私政策", "OIDC": "OIDC 令牌有效期和过期时间", + "WEB_KEYS": "OIDC Web Keys", "SECRETS": "验证码外观", "SECURITY": "安全设置", "EVENTS": "活动", @@ -1385,7 +1538,8 @@ "sv": "Svenska", "id": "Bahasa Indonesia", "hu": "Magyar", - "ko": "한국어" + "ko": "한국어", + "ro": "Română" } }, "SMTP": { @@ -1476,6 +1630,16 @@ "ACTIONS_DESCRIPTION": "Actions v2 可以管理数据执行和目标。如果启用此标志,您将可以使用新的 API 及其功能。", "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 终止会话", "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "如果启用了标志,您可以通过在 end_session 端点上提供带有 `sid` 声明的 id_token 作为 id_token_hint 来从登录 UI 终止单个会话。 请注意,目前所有来自同一用户代理(浏览器)的会话都在登录 UI 中终止。 通过会话 API 管理的会话已经允许终止单个会话。", + "DEBUGOIDCPARENTERROR": "调试 OIDC 父错误", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "如果启用该标志,OIDC 父错误将记录在控制台中。", + "DISABLEUSERTOKENEVENT": "禁用用户令牌事件", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "启用 Backchannel 注销", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel 注销实现了 OpenID Connect Back-Channel Logout 1.0,可用于通知客户端在 OpenID 提供商处终止会话。", + "PERMISSIONCHECKV2": "权限检查 V2", + "PERMISSIONCHECKV2_DESCRIPTION": "如果启用该标志,您将能够使用新的 API 及其功能。", + "WEBKEY": "Web 密钥", + "WEBKEY_DESCRIPTION": "如果启用该标志,您将能够使用新的 API 及其功能。", "STATES": { "INHERITED": "继承", "ENABLED": "已启用", @@ -1486,7 +1650,12 @@ "ENABLED": "“已启用” 是继承的", "DISABLED": "“已禁用” 是继承的" }, - "RESET": "全部设置为继承" + "RESET": "全部设置为继承", + "CONSOLEUSEV2USERAPI": "在控制台中使用V2 API创建用户。", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "启用此标志时,控制台使用V2用户API创建新用户。使用V2 API,新创建的用户将以无初始状态开始。", + "LOGINV2": "登录 V2", + "LOGINV2_DESCRIPTION": "启用此选项将激活基于 TypeScript 的新登录界面,具有更高的安全性、性能和可定制性。", + "LOGINV2_BASEURI": "基础 URI" }, "DIALOG": { "RESET": { @@ -1622,7 +1791,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "KEYS": { "emailVerificationDoneText": "电子邮件验证完成", @@ -2191,7 +2362,9 @@ "REMOVED": "成功删除。" }, "ISIDTOKENMAPPING": "从ID令牌映射", - "ISIDTOKENMAPPING_DESC": "如果选中,提供商信息将从ID令牌映射,而不是从userinfo端点。" + "ISIDTOKENMAPPING_DESC": "如果选中,提供商信息将从ID令牌映射,而不是从userinfo端点。", + "USEPKCE": "使用PKCE", + "USEPKCE_DESC": "确定 auth 请求中是否包含 code_challenge 和 code_challenge_method 参数" }, "MFA": { "LIST": { @@ -2570,7 +2743,9 @@ "nl": "Nederlands", "sv": "Svenska", "id": "Bahasa Indonesia", - "ko": "한국어" + "hu": "Magyar", + "ko": "한국어", + "ro": "Română" }, "MEMBER": { "ADD": "添加管理者", diff --git a/console/src/styles/table.scss b/console/src/styles/table.scss index 933366607e..3b177e07e7 100644 --- a/console/src/styles/table.scss +++ b/console/src/styles/table.scss @@ -133,6 +133,11 @@ color: if($is-dark-theme, #ffc1c1, #620e0e); background-color: if($is-dark-theme, map-get($background, state-inactive), #ffc1c1); } + + &.neutral { + background: if($is-dark-theme, #01489c78, #47a8ff82); + color: if($is-dark-theme, #47a8ff, #01489c); + } } .bg-state { diff --git a/console/yarn.lock b/console/yarn.lock index 73512cb091..5dd602dfa8 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -226,14 +226,14 @@ "@angular-eslint/bundled-angular-compiler" "18.0.0" "@typescript-eslint/utils" "8.0.0-alpha.20" -"@angular/animations@^16.2.5": +"@angular/animations@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-16.2.12.tgz#27744d8176e09e70e0f6d837c3abcfcee843a936" integrity sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w== dependencies: tslib "^2.3.0" -"@angular/cdk@^16.2.4": +"@angular/cdk@^16.2.14": version "16.2.14" resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-16.2.14.tgz#d26f8f1e7d2466b509e60489b6acf31bfe923acf" integrity sha512-n6PrGdiVeSTEmM/HEiwIyg6YQUUymZrb5afaNLGFRM5YL0Y8OBqd+XhCjb0OfD/AfgCUtedVEPwNqrfW8KzgGw== @@ -266,7 +266,7 @@ symbol-observable "4.0.0" yargs "17.7.2" -"@angular/common@^16.2.5": +"@angular/common@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/common/-/common-16.2.12.tgz#aa1d1522701833f1998001caa1ac95c3ac11d077" integrity sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w== @@ -287,31 +287,21 @@ tslib "^2.3.0" yargs "^17.2.1" -"@angular/compiler@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" - integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== - -"@angular/compiler@^16.2.5": +"@angular/compiler@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-16.2.12.tgz#d13366f190706c270b925495fbc12c29097e6b6c" integrity sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ== dependencies: tslib "^2.3.0" -"@angular/core@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" - integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== - -"@angular/core@^16.2.5": +"@angular/core@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/core/-/core-16.2.12.tgz#f664204275ee5f5eb46bddc0867e7a514731605f" integrity sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA== dependencies: tslib "^2.3.0" -"@angular/forms@^16.2.5": +"@angular/forms@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-16.2.12.tgz#a533ad61a65080281e709ca68840a1da9f189afc" integrity sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw== @@ -319,18 +309,18 @@ tslib "^2.3.0" "@angular/language-service@^18.2.4": - version "18.2.7" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-18.2.7.tgz#5aaf8158c8a07f261bef19911e92ee6e92223bb5" - integrity sha512-gFsme3y5uC/dQGBBX05VnmT2KAEAZ6gsNk8m1b226LYvh8Oc+JQ4sXv7THGq1x5VnrTzRcCIELbkNHCiFdvL1Q== + version "18.2.13" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-18.2.13.tgz#dd5555f7a74ffd5ba361e5bb615d6ea1e1719f93" + integrity sha512-4E4VJDrbOAxS69F9C1twQPbR9AjY47Qlz8+lwg5lJOyUJ4GoEThLbXKfadt/vIeYBwMJ7fIsYWXD0Dlmxh4k+w== -"@angular/material-moment-adapter@^16.2.4": +"@angular/material-moment-adapter@^16.2.14": version "16.2.14" resolved "https://registry.yarnpkg.com/@angular/material-moment-adapter/-/material-moment-adapter-16.2.14.tgz#d6972a50fcbb21483ba2888c577e443bebd0b6e6" integrity sha512-LagTDXEq8XOVLy8CVswCbmq7v9bb84+VikEEN09tz831U/7PHjDZ3xRgpKtv7hXrh8cTZOg3UPQw5tZk0hwh3Q== dependencies: tslib "^2.3.0" -"@angular/material@^16.2.4": +"@angular/material@^16.2.14": version "16.2.14" resolved "https://registry.yarnpkg.com/@angular/material/-/material-16.2.14.tgz#4db0c7d14d3d6ac6c8dac83dced0fb8a030b3b49" integrity sha512-zQIxUb23elPfiIvddqkIDYqQhAHa9ZwMblfbv+ug8bxr4D0Dw360jIarxCgMjAcLj7Ccl3GBqZMUnVeM6cjthw== @@ -384,28 +374,28 @@ "@material/typography" "15.0.0-canary.bc9ae6c9c.0" tslib "^2.3.0" -"@angular/platform-browser-dynamic@^16.2.5": +"@angular/platform-browser-dynamic@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.12.tgz#14488188c06013eb4153ac6e0603975f8b679f70" integrity sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw== dependencies: tslib "^2.3.0" -"@angular/platform-browser@^16.2.5": +"@angular/platform-browser@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-16.2.12.tgz#66b5611066cb3f8bb55f035658e978b50720f3b0" integrity sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ== dependencies: tslib "^2.3.0" -"@angular/router@^16.2.5": +"@angular/router@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/router/-/router-16.2.12.tgz#2f4cae64ddb7f998832aa340dd3f843cfb85cbc8" integrity sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA== dependencies: tslib "^2.3.0" -"@angular/service-worker@^16.2.5": +"@angular/service-worker@^16.2.12": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-16.2.12.tgz#359e72693de7d1e8015d1beb02689753ede96de6" integrity sha512-o0z0s4c76NmRASa+mUHn/q6vUKQNa06mGmLBDKm84vRQ1sQ2TJv+R1p8K9WkiM5mGy6tjQCDOgaz13TcxMFWOQ== @@ -417,7 +407,16 @@ resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.25.7": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/code-frame@^7.22.13", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== @@ -692,10 +691,10 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== -"@babel/helper-validator-identifier@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" - integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== +"@babel/helper-validator-identifier@^7.25.7", "@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== "@babel/helper-validator-option@^7.22.5", "@babel/helper-validator-option@^7.25.7": version "7.25.7" @@ -720,11 +719,11 @@ "@babel/types" "^7.25.7" "@babel/highlight@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.7.tgz#20383b5f442aa606e7b5e3043b0b1aafe9f37de5" - integrity sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw== + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" + integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw== dependencies: - "@babel/helper-validator-identifier" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.9" chalk "^2.4.2" js-tokens "^4.0.0" picocolors "^1.0.0" @@ -1459,53 +1458,79 @@ "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" -"@bufbuild/buf-darwin-arm64@1.43.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.43.0.tgz#6e32ac996657900a92bcf21574326d0375821269" - integrity sha512-IBVd4qpN8udUOwmqDB8KdGz8JUqVYq8yXN2MOj6JN7XFhaq0xAVfWsuQNOW4Uzukh+Ypg8EOb1nsJubOUbVe6g== +"@bufbuild/buf-darwin-arm64@1.50.1": + version "1.50.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.50.1.tgz#218bf184196f7458900f593ce76de696ea1a074e" + integrity sha512-uoRLgqucLP5qLIIDovsHN+7QoeBC0fqfZ1LhwZfNUd95py3ymEOmOFmP0JCbW349a8zmH2HhS5yOySe1yXwP2w== -"@bufbuild/buf-darwin-x64@1.43.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.43.0.tgz#331d239a51c97b735a584d57cf8fef8d57ecef94" - integrity sha512-XeuLdJIdOXzLpLA95k5NWhO6cJw5YW89IJW6OHENkxXkdf9aWzg9ozrhTpPzUoZmGZbNyXfpa7kbhJaJ5+F42w== +"@bufbuild/buf-darwin-x64@1.50.1": + version "1.50.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.50.1.tgz#5d3fd1b859227979a9c7bd148bf16ce8a6f0c956" + integrity sha512-b5efryjfQ/rTnftcjcbluAJK1bSHCacSK0O/ldMbNQRUwstUieqX/NHIxdQcrorHqW26VppWmQuB88JYxffABw== -"@bufbuild/buf-linux-aarch64@1.43.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.43.0.tgz#0f61c21a4863f898a77d67f897fed3128b17fba6" - integrity sha512-7hyXvwM5NFY08+aqbxVx4GZ/3AsqZKas3bR5TFLuD8/4u4JfA59b9+YMzLL1bxHFLqMMeXJBLgIVkyTtQIXGVg== +"@bufbuild/buf-linux-aarch64@1.50.1": + version "1.50.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.50.1.tgz#b96faa0f15b6eb956a39b60252c488f5afec0115" + integrity sha512-ZE3nfVyENkHNV+lZw9ScjmQnPNlzIZrGLYeqFTQllnbUWehvZACTBY8L4ak5eXf6NRyGIksAEuCPnnrht5WigA== -"@bufbuild/buf-linux-x64@1.43.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.43.0.tgz#05c6083f8afd97db668b939dbf6e6793da81b5a3" - integrity sha512-4mg6ZUNpW2C61vmPyQDolLRIsfAomd2Yv1PICC9XG1iQtf3xQ+6q5B/kihOadhG2AN31WeKdorvuyPL7vmbsDA== +"@bufbuild/buf-linux-armv7@1.50.1": + version "1.50.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.50.1.tgz#9b9c2ca41034c6f2f59d854585ea6a27ec578c3c" + integrity sha512-Bc/V1ySgFIaTa8sdCEwX3Y4vkyULlDeuUKyqUSSQjiVweeuoxOp8jLwQMFoQCdYFwY02yY0MlNR/mhyowGCt9w== -"@bufbuild/buf-win32-arm64@1.43.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.43.0.tgz#c10b11b0ab3381ee8572ba565fe43f485bb09d4f" - integrity sha512-YKoiA5Ui8VBW0GaWthDLBYr1C3I4Fx/j9txrUpg3Fvf+iwWutWQuTdA6W95HnLzUyhT6KnBMbXmB+SzosW9iFg== +"@bufbuild/buf-linux-x64@1.50.1": + version "1.50.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.50.1.tgz#ac5daffde0b9f20df2289a488f9b0f5000689557" + integrity sha512-2aHe+LmsTIaOH3ViH8EcOVx4yStZznJYmZ/NQ/cV5kvD3JAVJv4u5MtTJAd6DDj9BmEhXtjXK9hWtRi7S0vF2w== -"@bufbuild/buf-win32-x64@1.43.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.43.0.tgz#5ad447825db671291cb28eea59f05dbff81398be" - integrity sha512-//t0ndbP7zkQDydwAXShCqsqgOGX/ktsS2g0+D9A5Aw/nyLTxBmfsN+7hPxL4o0qq2N4MKs6DJeP4TvjYKBaMA== +"@bufbuild/buf-win32-arm64@1.50.1": + version "1.50.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.50.1.tgz#3d276ba4b9102dcb73291fa4e471de305b5d6b04" + integrity sha512-fqNQ9Ke/arleUulnePEyG2+iW2eB6xO6rcVovFY4EorZUTZ/EzrtdmVn87AfWJle3HxcXcIXP0onrcKBrwXUuQ== + +"@bufbuild/buf-win32-x64@1.50.1": + version "1.50.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.50.1.tgz#ccf0b131881b7cf56b0c11954ab397ccafa6f962" + integrity sha512-+jd1Dopk24WQtcSgden+dOtGu/OMAuBtnsfkv59pqbCbdZqNCsqrC7VluJ4/5y5PiNcB7hcA26RETkPtkInluA== "@bufbuild/buf@^1.41.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.43.0.tgz#38d65fb0063c11e98cdc44cbe2dc576275425079" - integrity sha512-lWiH+QJ8l0TGWP1KELvIA5BvAsZc0IiXppJlwI8EdQ2vQp39Ue2Xub8b13uy6IH0VDm9azQEjXuY1FxdB4mqsA== + version "1.50.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.50.1.tgz#679c7c0fccea02b784eb7f6cfd213cae58defec0" + integrity sha512-KVXBUfKe13STjoSZ9VPzomJUwYnJ01c1b54nJf4a90H/tg0Q/K0z0Wxz0Dr43vuuoDvVHAuBOZPs/XIwSH33IQ== optionalDependencies: - "@bufbuild/buf-darwin-arm64" "1.43.0" - "@bufbuild/buf-darwin-x64" "1.43.0" - "@bufbuild/buf-linux-aarch64" "1.43.0" - "@bufbuild/buf-linux-x64" "1.43.0" - "@bufbuild/buf-win32-arm64" "1.43.0" - "@bufbuild/buf-win32-x64" "1.43.0" + "@bufbuild/buf-darwin-arm64" "1.50.1" + "@bufbuild/buf-darwin-x64" "1.50.1" + "@bufbuild/buf-linux-aarch64" "1.50.1" + "@bufbuild/buf-linux-armv7" "1.50.1" + "@bufbuild/buf-linux-x64" "1.50.1" + "@bufbuild/buf-win32-arm64" "1.50.1" + "@bufbuild/buf-win32-x64" "1.50.1" + +"@bufbuild/protobuf@^2.2.2": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.2.3.tgz#9cd136f6b687e63e9b517b3a54211ece942897ee" + integrity sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg== "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== +"@connectrpc/connect-node@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@connectrpc/connect-node/-/connect-node-2.0.0.tgz#1ea4eff7f2633fbe3d80378e1420bd213d9d83a4" + integrity sha512-DoI5T+SUvlS/8QBsxt2iDoUg15dSxqhckegrgZpWOtADtmGohBIVbx1UjtWmjLBrP4RdD0FeBw+XyRUSbpKnJQ== + +"@connectrpc/connect-web@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@connectrpc/connect-web/-/connect-web-2.0.2.tgz#02f109e21eb06b31ee2558eeed39b1bf03cc9089" + integrity sha512-QANMFPiL2o66BdBEctg4TsQLe5ozsBLqcle3dCBp7BwGlNGTY6NnNnqmt+YRnpeMW88GgomJwWNMGCrRD9pRKA== + +"@connectrpc/connect@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@connectrpc/connect/-/connect-2.0.2.tgz#acd05e1408737c860e732137dc6f960321d2c628" + integrity sha512-xZuylIUNvNlH52e/4eQsZvY4QZyDJRtEFEDnn/yBrv5Xi5ZZI/p8X+GAHH35ucVaBvv9u7OzHZo8+tEh1EFTxA== + "@ctrl/ngx-codemirror@^6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@ctrl/ngx-codemirror/-/ngx-codemirror-6.1.0.tgz#9324a56e4b709be9c515364d21e05e1d7589f009" @@ -1783,48 +1808,30 @@ dependencies: tslib "^2.4.1" -"@fortawesome/fontawesome-common-types@6.6.0": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz#31ab07ca6a06358c5de4d295d4711b675006163f" - integrity sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw== +"@fortawesome/fontawesome-common-types@6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz#7123d74b0c1e726794aed1184795dbce12186470" + integrity sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg== -"@fortawesome/fontawesome-svg-core@^6.4.2": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz#2a24c32ef92136e98eae2ff334a27145188295ff" - integrity sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg== +"@fortawesome/fontawesome-svg-core@^6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz#0ac6013724d5cc327c1eb81335b91300a4fce2f2" + integrity sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA== dependencies: - "@fortawesome/fontawesome-common-types" "6.6.0" + "@fortawesome/fontawesome-common-types" "6.7.2" -"@fortawesome/free-brands-svg-icons@^6.4.2": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz#2797f2cc66d21e7e47fa64e680b8835e8d30e825" - integrity sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ== +"@fortawesome/free-brands-svg-icons@^6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz#4ebee8098f31da5446dda81edc344025eb59b27e" + integrity sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q== dependencies: - "@fortawesome/fontawesome-common-types" "6.6.0" + "@fortawesome/fontawesome-common-types" "6.7.2" "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@grpc/grpc-js@^1.11.2": - version "1.11.3" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.11.3.tgz#a33a472618d166fbb195012ae390dbfc277470ed" - integrity sha512-i9UraDzFHMR+Iz/MhFLljT+fCpgxZ3O6CxwGJ8YuNYHJItIHUzKJpW2LvoFZNnGPwqc9iWy9RAucxV0JoR9aUQ== - dependencies: - "@grpc/proto-loader" "^0.7.13" - "@js-sdsl/ordered-map" "^4.4.2" - -"@grpc/proto-loader@^0.7.13": - version "0.7.13" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" - integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== - dependencies: - lodash.camelcase "^4.3.0" - long "^5.0.0" - protobufjs "^7.2.5" - yargs "^17.7.2" - "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -1912,11 +1919,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@js-sdsl/ordered-map@^4.4.2": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" - integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== - "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" @@ -2625,19 +2627,18 @@ tslib "^2.1.0" "@netlify/framework-info@^9.8.13": - version "9.8.13" - resolved "https://registry.yarnpkg.com/@netlify/framework-info/-/framework-info-9.8.13.tgz#0a4cc2be4c2439089f9b630e19d73e2f4b09289d" - integrity sha512-ZZXCggokY/y5Sz93XYbl/Lig1UAUSWPMBiQRpkVfbrrkjmW2ZPkYS/BgrM2/MxwXRvYhc/TQpZX6y5JPe3quQg== + version "9.9.2" + resolved "https://registry.yarnpkg.com/@netlify/framework-info/-/framework-info-9.9.2.tgz#e25e45eefa2ecababc0bad349f95710747514d42" + integrity sha512-IIJQ/mMCv7IvGRKujVXH9Jbyb19LCVUaFWoABljmbMHmALSFIL0twGx30SuHUPR1cXK6fH7KK5r4UsyvkpJ3EA== dependencies: ajv "^8.12.0" filter-obj "^5.0.0" find-up "^6.3.0" is-plain-obj "^4.0.0" locate-path "^7.0.0" - p-filter "^3.0.0" + p-filter "^4.0.0" p-locate "^6.0.0" - process "^0.11.10" - read-pkg-up "^9.1.0" + read-package-up "^11.0.0" semver "^7.3.8" "@ngtools/webpack@16.2.16": @@ -2828,59 +2829,6 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== - "@schematics/angular@16.2.16": version "16.2.16" resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.16.tgz#3f077f398fc7ff88654fd477790af8270585c3af" @@ -3063,11 +3011,16 @@ dependencies: "@types/node" "*" -"@types/jasmine@*", "@types/jasmine@~5.1.4": +"@types/jasmine@*": version "5.1.4" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-5.1.4.tgz#0de3f6ca753e10d1600ce1864ae42cfd47cf9924" integrity sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w== +"@types/jasmine@~5.1.4": + version "5.1.7" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-5.1.7.tgz#b9f68cf05463717bbe36d53221839e7630a24d22" + integrity sha512-DVOfk9FaClQfNFpSfaML15jjB5cjffDMvjtph525sroR5BEAW2uKnTOYUTqTFuZFjNvH0T5XMIydvIctnUKufw== + "@types/jasminewd2@~2.0.13": version "2.0.13" resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.13.tgz#0b60c1fcd06277ea97efbbad5a02e0c1a4a8996a" @@ -3081,10 +3034,11 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/jsonwebtoken@^9.0.6": - version "9.0.7" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz#e49b96c2b29356ed462e9708fc73b833014727d2" - integrity sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg== + version "9.0.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3" + integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ== dependencies: + "@types/ms" "*" "@types/node" "*" "@types/mime@^1": @@ -3092,6 +3046,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + "@types/node-forge@^1.3.0": version "1.3.11" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" @@ -3099,14 +3058,21 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.7.0", "@types/node@^22.5.5": +"@types/node@*", "@types/node@^22.5.5": + version "22.13.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.13.tgz#5e7d110fb509b0d4a43fbf48fa9d6e0f83e1b1e7" + integrity sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ== + dependencies: + undici-types "~6.20.0" + +"@types/node@>=10.0.0": version "22.7.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc" integrity sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg== dependencies: undici-types "~6.19.2" -"@types/normalize-package-data@^2.4.1": +"@types/normalize-package-data@^2.4.3": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== @@ -3116,11 +3082,6 @@ resolved "https://registry.yarnpkg.com/@types/opentype.js/-/opentype.js-1.3.8.tgz#741be92429d1c2d64b5fa79cf692f74b49d6007f" integrity sha512-H6qeTp03jrknklSn4bpT1/9+1xCAEIU2CnjcWPkicJy8A1SKuthanbvoHYMiv79/2W3Xn1XE4gfSJFzt2U3JSw== -"@types/q@^0.0.32": - version "0.0.32" - resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" - integrity sha512-qYi3YV9inU/REEfxwVcGZzbS3KG/Xs90lv0Pr+lDtuVjBPGd1A+eciXzVSaRvLify132BfcvhvEjeVahrUl0Ug== - "@types/qrcode@^1.5.2": version "1.5.5" resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" @@ -3143,11 +3104,6 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/selenium-webdriver@^3.0.0": - version "3.0.26" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.26.tgz#fc7d87d580affa2e52685b2e881bc201819a5836" - integrity sha512-dyIGFKXfUFiwkMfNGn1+F6b80ZjR3uSYv1j6xVJSDlft5waZ2cwkHW4e7zNzvq7hiEackcgvBpmnXZrI1GltPg== - "@types/semver@^7.3.12": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" @@ -3219,7 +3175,7 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.60.1": +"@typescript-eslint/parser@^5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== @@ -3496,6 +3452,32 @@ js-yaml "^3.10.0" tslib "^2.4.0" +"@zitadel/client@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.7.tgz#39dc8d3d10bfa01e5cf56205ba188f79c39f052d" + integrity sha512-sZG4NEa8vQBt3+4W1AesY+5DstDBuZiqGH2EM+UqbO5D93dlDZInXqZ5oRE7RSl2Bk5ED9mbMFrB7b8DuRw72A== + dependencies: + "@bufbuild/protobuf" "^2.2.2" + "@connectrpc/connect" "^2.0.0" + "@connectrpc/connect-node" "^2.0.0" + "@connectrpc/connect-web" "^2.0.0" + "@zitadel/proto" "1.0.4" + jose "^5.3.0" + +"@zitadel/proto@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.4.tgz#e2fe9895f2960643c3619191255aa2f4913ad873" + integrity sha512-s13ZMhuOTe0b+geV+JgJud+kpYdq7TgkuCe7RIY+q4Xs5KC0FHMKfvbAk/jpFbD+TSQHiwo/TBNZlGHdwUR9Ig== + dependencies: + "@bufbuild/protobuf" "^2.2.2" + +"@zitadel/proto@1.0.5-sha-4118a9d": + version "1.0.5-sha-4118a9d" + resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.5-sha-4118a9d.tgz#e09025f31b2992b061d5416a0d1e12ef370118cc" + integrity sha512-7ZFwISL7TqdCkfEUx7/H6UJDqX8ZP2jqG1ulbELvEQ2smrK365Zs7AkJGeB/xbVdhQW9BOhWy2R+Jni7sfxd2w== + dependencies: + "@bufbuild/protobuf" "^2.2.2" + "@zkochan/js-yaml@0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826" @@ -3562,11 +3544,6 @@ adjust-sourcemap-loader@^4.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -adm-zip@^0.5.2: - version "0.5.16" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" - integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== - agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -3574,13 +3551,6 @@ agent-base@6, agent-base@^6.0.2: dependencies: debug "4" -agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - agentkeepalive@^4.2.1: version "4.5.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" @@ -3596,14 +3566,6 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -aggregate-error@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-4.0.1.tgz#25091fe1573b9e0be892aeda15c7c66a545f758e" - integrity sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w== - dependencies: - clean-stack "^4.0.0" - indent-string "^5.0.0" - ajv-formats@2.1.1, ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -3633,7 +3595,7 @@ ajv@8.12.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3660,7 +3622,7 @@ angular-oauth2-oidc@^15.0.1: dependencies: tslib "^2.0.0" -angularx-qrcode@^16.0.0: +angularx-qrcode@^16.0.2: version "16.0.2" resolved "https://registry.yarnpkg.com/angularx-qrcode/-/angularx-qrcode-16.0.2.tgz#86af924191546394cb93f9fb8d0c42edd0132894" integrity sha512-FztOM7vjNu88sGxUU5jG2I+A9TxZBXXYBWINjpwIBbTL+COMgrtzXnScG7TyQeNknv5w3WFJWn59PcngRRYVXA== @@ -3685,11 +3647,6 @@ ansi-html-community@^0.0.8: resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -3700,11 +3657,6 @@ ansi-regex@^6.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== - ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -3732,11 +3684,6 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -app-root-path@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86" - integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA== - "aproba@^1.0.3 || ^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" @@ -3769,58 +3716,16 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" -aria-query@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" - integrity sha512-majUxHgLehQTeSA+hClx+DY09OVUqG3GtezWkF1krgLGNdlDu9l9V8DaqNMWbq4Eddc8wsyDA0hpDUtnYxQEXw== - dependencies: - ast-types-flow "0.0.7" - commander "^2.11.0" - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== - dependencies: - array-uniq "^1.0.1" - array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== - -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== - -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== - -ast-types-flow@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== - async@^3.2.3: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" @@ -3843,16 +3748,6 @@ autoprefixer@10.4.14: picocolors "^1.0.0" postcss-value-parser "^4.2.0" -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== - -aws4@^1.8.0: - version "1.13.2" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" - integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== - axios@^1.0.0: version "1.7.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" @@ -3862,13 +3757,6 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" -axobject-query@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" - integrity sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww== - dependencies: - ast-types-flow "0.0.7" - axobject-query@3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -3946,13 +3834,6 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -3972,13 +3853,6 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -blocking-proxy@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/blocking-proxy/-/blocking-proxy-1.0.1.tgz#81d6fd1fe13a4c0d6957df7f91b75e98dac40cb2" - integrity sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA== - dependencies: - minimist "^1.2.0" - body-parser@1.20.3, body-parser@^1.19.0: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -4047,13 +3921,6 @@ browserslist@^4.21.10, browserslist@^4.21.5, browserslist@^4.23.3, browserslist@ node-releases "^2.0.18" update-browserslist-db "^1.1.0" -browserstack@^1.5.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.1.tgz#e051f9733ec3b507659f395c7a4765a1b1e358b3" - integrity sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw== - dependencies: - https-proxy-agent "^2.2.1" - buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -4153,22 +4020,6 @@ caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001663: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz#112d77e80f1762f62a1b71ba92164e0cb3f3dd13" integrity sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g== -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== - -chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -4236,13 +4087,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clean-stack@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-4.2.0.tgz#c464e4cde4ac789f4e0735c5d75beb49d7b30b31" - integrity sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg== - dependencies: - escape-string-regexp "5.0.0" - cli-cursor@3.1.0, cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -4306,30 +4150,10 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -codelyzer@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-6.0.2.tgz#25d72eae641e8ff13ffd7d99b27c9c7ad5d7e135" - integrity sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g== - dependencies: - "@angular/compiler" "9.0.0" - "@angular/core" "9.0.0" - app-root-path "^3.0.0" - aria-query "^3.0.0" - axobject-query "2.0.2" - css-selector-tokenizer "^0.7.1" - cssauron "^1.4.0" - damerau-levenshtein "^1.0.4" - rxjs "^6.5.3" - semver-dsl "^1.0.1" - source-map "^0.5.7" - sprintf-js "^1.1.2" - tslib "^1.10.0" - zone.js "~0.10.3" - -codemirror@^5.65.8: - version "5.65.18" - resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.18.tgz#d7146e4271135a9b4adcd023a270185457c9c428" - integrity sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA== +codemirror@^5.65.19: + version "5.65.19" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.19.tgz#71016c701d6a4b6e1982b0f6e7186be65e49653d" + integrity sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA== color-convert@^1.9.0: version "1.9.3" @@ -4370,14 +4194,14 @@ colors@1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.20.0: +commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -4496,21 +4320,16 @@ core-js-compat@^3.31.0, core-js-compat@^3.33.1: browserslist "^4.23.3" core-js@^3.38.1: - version "3.39.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83" - integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g== - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + version "3.41.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.41.0.tgz#57714dafb8c751a6095d028a7428f1fb5834a776" + integrity sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA== core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cors@^2.8.5, cors@~2.8.5: +cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -4575,26 +4394,11 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" -css-selector-tokenizer@^0.7.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz#735f26186e67c749aaf275783405cf0661fae8f1" - integrity sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg== - dependencies: - cssesc "^3.0.0" - fastparse "^1.1.2" - css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -cssauron@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cssauron/-/cssauron-1.4.0.tgz#a6602dff7e04a8306dc0db9a551e92e8b5662ad8" - integrity sha512-Ht70DcFBh+/ekjVrYS2PlDMdSQEl3OFNmjK6lcn49HptBgilXf/Zwg4uFh9Xn0pX3Q8YOkSjIFOfK2osvdqpBw== - dependencies: - through X.X.X - cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4622,18 +4426,6 @@ custom-event@~1.0.0: resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== -damerau-levenshtein@^1.0.4: - version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" - integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== - dependencies: - assert-plus "^1.0.0" - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -4662,13 +4454,6 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, d dependencies: ms "^2.1.3" -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -4712,19 +4497,6 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -del@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" - integrity sha512-Z4fzpbIRjOu7lO5jCETSWoqUDVe0IPOlfugBsF6suen2LKDlVb4QZpKEM9P+buNJ4KI1eN7I083w/pbKUpsrWQ== - dependencies: - globby "^5.0.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - rimraf "^2.2.8" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -4858,14 +4630,6 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -5016,18 +4780,6 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== - dependencies: - es6-promise "^4.0.3" - esbuild-wasm@0.18.17: version "0.18.17" resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.18.17.tgz#d3d8827502c7714212a7b2544ee99132f07189cc" @@ -5099,12 +4851,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-string-regexp@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" - integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== @@ -5154,7 +4901,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.50.0: +eslint@^8.57.1: version "8.57.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -5276,11 +5023,6 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - exponential-backoff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" @@ -5323,7 +5065,7 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" -extend@^3.0.0, extend@~3.0.2: +extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -5337,16 +5079,6 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5396,14 +5128,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024" - integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row== - -fastparse@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" - integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== fastq@^1.6.0: version "1.17.1" @@ -5496,6 +5223,11 @@ find-cache-dir@^4.0.0: common-path-prefix "^3.0.0" pkg-dir "^7.0.0" +find-up-simple@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/find-up-simple/-/find-up-simple-1.0.1.tgz#18fb90ad49e45252c4d7fca56baade04fa3fca1e" + integrity sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ== + find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -5520,10 +5252,10 @@ find-up@^6.3.0: locate-path "^7.1.0" path-exists "^5.0.0" -flag-icons@^7.1.0: - version "7.2.3" - resolved "https://registry.yarnpkg.com/flag-icons/-/flag-icons-7.2.3.tgz#b67f379fa0ef28c4e605319a78035131bdd8ced7" - integrity sha512-X2gUdteNuqdNqob2KKTJTS+ZCvyWeLCtDz9Ty8uJP17Y4o82Y+U/Vd4JNrdwTAjagYsRznOn9DZ+E/Q52qbmqg== +flag-icons@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/flag-icons/-/flag-icons-7.3.2.tgz#c5ffaf25fb2be97498703a068fa699416bca9629" + integrity sha512-QkaZ6Zvai8LIjx+UNAHUJ5Dhz9OLZpBDwCRWxF6YErxIcR16jTkIFm3bFu54EkvKQy4+wicW+Gm7/0631wVQyQ== flat-cache@^3.0.4: version "3.2.0" @@ -5557,11 +5289,6 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -5580,15 +5307,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5706,13 +5424,6 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" - glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -5756,7 +5467,7 @@ glob@^10.2.2: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.0.3, glob@^7.0.6, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: +glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5814,27 +5525,7 @@ globby@^13.1.1: merge2 "^1.4.1" slash "^4.0.0" -globby@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" - integrity sha512-HJRTIH2EeH44ka+LWig+EqT2ONSYpVlNfx6pyd592/VF1TbfljJ7elwie7oSwcViLGqOdWocSdu2txwBF9bjmQ== - dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -google-proto-files@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/google-proto-files/-/google-proto-files-4.2.0.tgz#130a6caa307b02541cbc6005234e3fe156768027" - integrity sha512-Yl3ZtTSpkOLjHTqHn91NhDp2jMPzpHWowSGz3S30N6gkqOXrJwUu44alR9dX+NyHK3n165uR+jezOH365b1pPA== - dependencies: - protobufjs "^7.0.0" - walkdir "^0.4.0" - -google-protobuf@^3.21.2: +google-protobuf@^3.21.4: version "3.21.4" resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.4.tgz#2f933e8b6e5e9f8edde66b7be0024b68f77da6c9" integrity sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ== @@ -5856,7 +5547,7 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -grpc-web@^1.4.1: +grpc-web@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/grpc-web/-/grpc-web-1.5.0.tgz#154e4007ab59a94bf7726b87ef6c5bd8815ecf6e" integrity sha512-y1tS3BBIoiVSzKTDF3Hm7E8hV2n7YY7pO0Uo7depfWJqKzWE+SKr0jvHNIJsJJYILQlpYShpi/DRJJMbosgDMQ== @@ -5873,26 +5564,6 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== - dependencies: - ansi-regex "^2.0.0" - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -5946,13 +5617,6 @@ hdr-histogram-percentiles-obj@^3.0.0: resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw== -hosted-git-info@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" - integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== - dependencies: - lru-cache "^6.0.0" - hosted-git-info@^6.0.0: version "6.1.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" @@ -5960,6 +5624,13 @@ hosted-git-info@^6.0.0: dependencies: lru-cache "^7.5.1" +hosted-git-info@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" + integrity sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w== + dependencies: + lru-cache "^10.0.1" + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -6071,15 +5742,6 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -6088,14 +5750,6 @@ https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -6108,10 +5762,10 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -i18n-iso-countries@^7.7.0: - version "7.12.0" - resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.12.0.tgz#e189d85a505ee025f0f48b5ccc35fa66c436e99c" - integrity sha512-NDFf5j/raA5JrcPT/NcHP3RUMH7TkdkxQKAKdvDlgb+MS296WJzzqvV0Y5uwavSm7A6oYvBeSV0AxoHdDiHIiw== +i18n-iso-countries@^7.14.0: + version "7.14.0" + resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz#cd5ae098198bce1cc40cadbf0a37ce6c8e9d0edf" + integrity sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg== dependencies: diacritics "1.3.0" @@ -6161,11 +5815,6 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== - immutable@^4.0.0: version "4.3.7" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" @@ -6189,10 +5838,10 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -indent-string@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" - integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== +index-to-position@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/index-to-position/-/index-to-position-1.0.0.tgz#baca236eb6e8c2b750b9225313c31751f84ef357" + integrity sha512-sCO7uaLVhRJ25vz1o8s9IFM3nVS4DkuQnyjMwiQPKvQuBYBDmb8H7zx8ki7nVh4HJQOdVWebyvLE0qt+clruxA== infer-owner@^1.0.4: version "1.0.4" @@ -6222,11 +5871,6 @@ ini@4.1.1: resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== -ini@^1.3.4: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - inquirer@8.2.4: version "8.2.4" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" @@ -6278,7 +5922,7 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.8.1: +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.8.1: version "2.15.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== @@ -6322,25 +5966,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" - integrity sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw== - -is-path-in-cwd@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" - integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== - dependencies: - is-path-inside "^1.0.0" - -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g== - dependencies: - path-is-inside "^1.0.1" - is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -6373,11 +5998,6 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -6415,11 +6035,6 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" @@ -6493,15 +6108,10 @@ jasmine-core@^4.1.0: resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-4.6.1.tgz#5ebb8afa07282078f8d7b15871737a83b74e58f2" integrity sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ== -jasmine-core@~2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" - integrity sha512-SNkOkS+/jMZvLhuSx1fjhcNWUC/KG6oVyFUGkSBEr9n1axSNduWU8GlI7suaHXr4yxjet6KjrUZxUTE5WzzWwQ== - -jasmine-core@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.3.0.tgz#ed784e5a10af43988d8408bad80b9f08e240a3f8" - integrity sha512-zsOmeBKESky4toybvWEikRiZ0jHoBEu79wNArLfMdSnlLMZx3Xcp6CSm2sUcYyoJC+Uyj8LBJap/MUbVSfJ27g== +jasmine-core@~5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.6.0.tgz#4b979c254e7d9b1fe8e767ab00c5d2901c00bd4f" + integrity sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA== jasmine-spec-reporter@~7.0.0: version "7.0.0" @@ -6510,20 +6120,6 @@ jasmine-spec-reporter@~7.0.0: dependencies: colors "1.4.0" -jasmine@2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" - integrity sha512-KbdGQTf5jbZgltoHs31XGiChAPumMSY64OZMWLNYnEnMfG5uwGBhffePwuskexjT+/Jea/gU3qAU8344hNohSw== - dependencies: - exit "^0.1.2" - glob "^7.0.6" - jasmine-core "~2.8.0" - -jasminewd2@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" - integrity sha512-Rn0nZe4rfDhzA63Al3ZGh0E+JTmM6ESZYXJGKuqKGZObsAB9fwXPD03GjtIEvJBDOhN94T5MzbwZSqzFHSQPzg== - jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" @@ -6538,6 +6134,11 @@ jiti@^1.18.2: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== +jose@^5.3.0: + version "5.9.6" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.6.tgz#77f1f901d88ebdc405e57cce08d2a91f47521883" + integrity sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6563,11 +6164,6 @@ jsbn@1.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - jsdom@^16.4.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -6636,21 +6232,11 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -6682,26 +6268,6 @@ jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -jszip@^3.1.3: - version "3.10.1" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" - integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== - dependencies: - lie "~3.3.0" - pako "~1.0.2" - readable-stream "~2.3.6" - setimmediate "^1.0.5" - karma-chrome-launcher@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" @@ -6739,7 +6305,7 @@ karma-source-map-support@1.4.0: dependencies: source-map-support "^0.5.5" -karma@^6.4.2: +karma@^6.4.4: version "6.4.4" resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.4.tgz#dfa5a426cf5a8b53b43cd54ef0d0d09742351492" integrity sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w== @@ -6826,10 +6392,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.11.8: - version "1.11.10" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.10.tgz#d9ccd71a227bfb9775eb0516744c26d8c0f4b057" - integrity sha512-yHEtzDlG2VbhuxMIo/sAUY+lyL5YThqD+gHT3YyelaRWRQv1VeyEk94jDvviRT4PYmLJR9cHxvtrx9h2f4OAIQ== +libphonenumber-js@^1.12.6: + version "1.12.6" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz#32a211b976dde3ccdf201c3c0b6e60351167c8bf" + integrity sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw== license-webpack-plugin@4.0.2: version "4.0.2" @@ -6838,13 +6404,6 @@ license-webpack-plugin@4.0.2: dependencies: webpack-sources "^3.0.0" -lie@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" - integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== - dependencies: - immediate "~3.0.5" - lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -6895,11 +6454,6 @@ locate-path@^7.0.0, locate-path@^7.1.0: dependencies: p-locate "^6.0.0" -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -6934,12 +6488,7 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.1.5" -long@^5.0.0: - version "5.2.3" - resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" - integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== - -lru-cache@^10.2.0: +lru-cache@^10.0.1, lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== @@ -7033,7 +6582,7 @@ material-colors@^1.2.6: resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== -material-design-icons-iconfont@^6.1.1: +material-design-icons-iconfont@^6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz#55cf0f3d7e4c76e032855b7e810b6e30535eff3c" integrity sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA== @@ -7088,7 +6637,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -7250,7 +6799,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment@^2.29.4: +moment@^2.30.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== @@ -7265,7 +6814,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -7377,16 +6926,6 @@ nopt@^6.0.0: dependencies: abbrev "^1.0.0" -normalize-package-data@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" - integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== - dependencies: - hosted-git-info "^4.0.1" - is-core-module "^2.5.0" - semver "^7.3.4" - validate-npm-package-license "^3.0.1" - normalize-package-data@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-5.0.0.tgz#abcb8d7e724c40d88462b84982f7cbf6859b4588" @@ -7397,6 +6936,15 @@ normalize-package-data@^5.0.0: semver "^7.3.5" validate-npm-package-license "^3.0.4" +normalize-package-data@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-6.0.2.tgz#a7bc22167fe24025412bcff0a9651eb768b03506" + integrity sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g== + dependencies: + hosted-git-info "^7.0.0" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -7556,12 +7104,7 @@ nx@16.5.1: "@nx/nx-win32-arm64-msvc" "16.5.1" "@nx/nx-win32-x64-msvc" "16.5.1" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4, object-assign@^4.0.1: +object-assign@^4: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -7658,17 +7201,17 @@ ora@5.4.1, ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== -p-filter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-3.0.0.tgz#ce50e03b24b23930e11679ab8694bd09a2d7ed35" - integrity sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg== +p-filter@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-4.1.0.tgz#fe0aa794e2dfad8ecf595a39a245484fcd09c6e4" + integrity sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw== dependencies: - p-map "^5.1.0" + p-map "^7.0.1" p-limit@^2.2.0: version "2.3.0" @@ -7719,12 +7262,10 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-map@^5.1.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-5.5.0.tgz#054ca8ca778dfa4cf3f8db6638ccb5b937266715" - integrity sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg== - dependencies: - aggregate-error "^4.0.0" +p-map@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.3.tgz#7ac210a2d36f81ec28b736134810f7ba4418cdb6" + integrity sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA== p-retry@^4.5.0: version "4.6.2" @@ -7768,7 +7309,7 @@ pacote@15.2.0: ssri "^10.0.0" tar "^6.1.11" -pako@^1.0.3, pako@~1.0.2: +pako@^1.0.3: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -7790,6 +7331,15 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-json@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-8.2.0.tgz#794a590dcf54588ec2282ce6065f15121fa348a0" + integrity sha512-eONBZy4hm2AgxjNFd8a4nyDJnzUAH0g34xSQAwWEVGCjdZ4ZL7dKZBfq267GWP/JaS9zW62Xs2FeAdDvpHHJGQ== + dependencies: + "@babel/code-frame" "^7.26.2" + index-to-position "^1.0.0" + type-fest "^4.37.0" + parse-node-version@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -7843,11 +7393,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -7876,12 +7421,12 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +picocolors@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picocolors@^1.0.0, picocolors@^1.1.0: +picocolors@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== @@ -7891,28 +7436,11 @@ picomatch@2.3.1, picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== - piscina@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.0.0.tgz#f8913d52b2000606d51aaa242f0813a0c77ca3b1" @@ -8004,35 +7532,35 @@ postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.27: picocolors "^1.1.0" source-map-js "^1.2.1" -posthog-js@^1.191.0: - version "1.191.0" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.191.0.tgz#150d0d8d6b4c4afbb42f769ff49d2e3fae5fa588" - integrity sha512-RSyqE9GRb6nqJa/DurtdXSlpDi15RUDAyDh13n2CvaiI5Ij7eIs5gEmfqJZchVZgJhtiOCZ5l6/zAq2NxKqbrg== +posthog-js@^1.232.7: + version "1.232.7" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.232.7.tgz#76c0bb565d82f1567c47118b164fc8dbb795c48c" + integrity sha512-9Hqdam80bf41mj30O3ZLgdfDtZqR9TXNpTh7EkJo6C/emerFQEnW7X+E3yyE81KIPON4amSqTj5BL0gg+qZ28w== dependencies: core-js "^3.38.1" fflate "^0.4.8" preact "^10.19.3" - web-vitals "^4.2.0" + web-vitals "^4.2.4" preact@^10.19.3: - version "10.25.0" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.25.0.tgz#22a1c93ce97336c5d01d74f363433ab0cd5cde64" - integrity sha512-6bYnzlLxXV3OSpUxLdaxBmE7PMOu0aR3pG6lryK/0jmvcDFPlcXGQAt5DpK3RITWiDrfYZRI0druyaK/S9kYLg== + version "10.26.4" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.26.4.tgz#b514f4249453a4247c82ff6d1267d59b7d78f9f9" + integrity sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w== prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-plugin-organize-imports@^4.0.0: +prettier-plugin-organize-imports@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz#f3d3764046a8e7ba6491431158b9be6ffd83b90f" integrity sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A== -prettier@^3.1.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== +prettier@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5" + integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw== pretty-bytes@^5.3.0: version "5.6.0" @@ -8049,11 +7577,6 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -8067,45 +7590,6 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -protobufjs@^7.0.0, protobufjs@^7.2.5: - version "7.4.0" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" - integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" - -protractor@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/protractor/-/protractor-7.0.0.tgz#c3e263608bd72e2c2dc802b11a772711a4792d03" - integrity sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw== - dependencies: - "@types/q" "^0.0.32" - "@types/selenium-webdriver" "^3.0.0" - blocking-proxy "^1.0.0" - browserstack "^1.5.1" - chalk "^1.1.3" - glob "^7.0.3" - jasmine "2.8.0" - jasminewd2 "^2.1.0" - q "1.4.1" - saucelabs "^1.5.0" - selenium-webdriver "3.6.0" - source-map-support "~0.4.0" - webdriver-js-extender "2.1.0" - webdriver-manager "^12.1.7" - yargs "^15.3.1" - proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -8124,7 +7608,7 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== -psl@^1.1.28, psl@^1.1.33: +psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -8139,16 +7623,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -q@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" - integrity sha512-/CdEdaw49VZVmyIDGUQKDDT53c7qBkO6g5CefWz91Ae+l4+cRtcDYwMTXh6me4O8TMldeGHG3N2Bl84V78Ywbg== - -q@^1.4.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== - qjobs@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" @@ -8171,11 +7645,6 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -8226,26 +7695,27 @@ read-package-json@^6.0.0: normalize-package-data "^5.0.0" npm-normalize-package-bin "^3.0.0" -read-pkg-up@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-9.1.0.tgz#38ca48e0bc6c6b260464b14aad9bcd4e5b1fbdc3" - integrity sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg== +read-package-up@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/read-package-up/-/read-package-up-11.0.0.tgz#71fb879fdaac0e16891e6e666df22de24a48d5ba" + integrity sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ== dependencies: - find-up "^6.3.0" - read-pkg "^7.1.0" - type-fest "^2.5.0" + find-up-simple "^1.0.0" + read-pkg "^9.0.0" + type-fest "^4.6.0" -read-pkg@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-7.1.0.tgz#438b4caed1ad656ba359b3e00fd094f3c427a43e" - integrity sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg== +read-pkg@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-9.0.1.tgz#b1b81fb15104f5dbb121b6bbdee9bbc9739f569b" + integrity sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA== dependencies: - "@types/normalize-package-data" "^2.4.1" - normalize-package-data "^3.0.2" - parse-json "^5.2.0" - type-fest "^2.0.0" + "@types/normalize-package-data" "^2.4.3" + normalize-package-data "^6.0.0" + parse-json "^8.0.0" + type-fest "^4.6.0" + unicorn-magic "^0.1.0" -readable-stream@^2.0.1, readable-stream@~2.3.6: +readable-stream@^2.0.1: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -8337,32 +7807,6 @@ regjsparser@^0.11.0: dependencies: jsesc "~3.0.2" -request@^2.87.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -8450,7 +7894,7 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== -rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.3: +rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -8483,31 +7927,31 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@7.8.1, rxjs@^7.5.5, rxjs@~7.8.0: +rxjs@7.8.1, rxjs@^7.5.5: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" -rxjs@^6.5.3: - version "6.6.7" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== +rxjs@^7.8.2: + version "7.8.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== dependencies: - tslib "^1.9.0" + tslib "^2.1.0" safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -8533,14 +7977,7 @@ sass@1.64.1: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -saucelabs@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d" - integrity sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ== - dependencies: - https-proxy-agent "^2.2.1" - -sax@>=0.6.0, sax@^1.2.4: +sax@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== @@ -8576,16 +8013,6 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== -selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz#2ba87a1662c020b8988c981ae62cb2a01298eafc" - integrity sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q== - dependencies: - jszip "^3.1.3" - rimraf "^2.5.4" - tmp "0.0.30" - xml2js "^0.4.17" - selfsigned@^2.1.1: version "2.4.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" @@ -8594,13 +8021,6 @@ selfsigned@^2.1.1: "@types/node-forge" "^1.3.0" node-forge "^1" -semver-dsl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/semver-dsl/-/semver-dsl-1.0.1.tgz#d3678de5555e8a61f629eed025366ae5f27340a0" - integrity sha512-e8BOaTo007E3dMuQQTnPdalbKTABKNS7UxoBIDnwOqRa+QwMrCPjynB8zAlPF6xlqUfdLPPLIJ13hJNmhtq8Ng== - dependencies: - semver "^5.3.0" - semver@7.5.3: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" @@ -8615,7 +8035,7 @@ semver@7.5.4: dependencies: lru-cache "^6.0.0" -semver@^5.3.0, semver@^5.6.0: +semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== @@ -8625,11 +8045,16 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0: +semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.6.0: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.3.8: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== + send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -8696,11 +8121,6 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -8858,13 +8278,6 @@ source-map-support@0.5.21, source-map-support@^0.5.5, source-map-support@~0.5.20 buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@~0.4.0: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -8875,11 +8288,6 @@ source-map@0.7.4: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== -source-map@^0.5.6, source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -8902,9 +8310,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.20" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" - integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== + version "3.0.21" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3" + integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg== spdy-transport@^3.0.0: version "3.0.0" @@ -8929,7 +8337,7 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -sprintf-js@^1.1.2, sprintf-js@^1.1.3: +sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== @@ -8939,21 +8347,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sshpk@^1.7.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" - integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - ssri@^10.0.0: version "10.0.6" resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" @@ -9040,13 +8433,6 @@ string_decoder@~1.1.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== - dependencies: - ansi-regex "^2.0.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -9085,11 +8471,6 @@ strong-log-transformer@^2.1.0: minimist "^1.2.0" through "^2.3.4" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -9199,7 +8580,7 @@ text-table@0.2.0, text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -through@X.X.X, through@^2.3.4, through@^2.3.6: +through@^2.3.4, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -9219,13 +8600,6 @@ tinycolor2@^1.6.0: resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== -tmp@0.0.30: - version "0.0.30" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" - integrity sha512-HXdTB7lvMwcb55XFfrTM8CPr/IYREk4hVBFaQ4b/6nInrluSL86hfHm7vu0luYKCfyBZp2trCjpc8caC3vVM3w== - dependencies: - os-tmpdir "~1.0.1" - tmp@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -9272,14 +8646,6 @@ tough-cookie@^4.0.0: universalify "^0.2.0" url-parse "^1.5.3" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -9311,16 +8677,21 @@ tslib@2.6.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== -tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.7.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1: version "2.7.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== +tslib@^2.1.0, tslib@^2.7.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -9337,18 +8708,6 @@ tuf-js@^1.1.7: debug "^4.3.4" make-fetch-happen "^11.1.1" -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -9366,10 +8725,10 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-fest@^2.0.0, type-fest@^2.5.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== +type-fest@^4.37.0, type-fest@^4.6.0: + version "4.38.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.38.0.tgz#659fa14d1a71c2811400aa3b5272627e0c1e6b96" + integrity sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg== type-is@~1.6.18: version "1.6.18" @@ -9399,6 +8758,11 @@ undici-types@~6.19.2: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" @@ -9422,6 +8786,11 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +unicorn-magic@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" + integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + unique-filename@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" @@ -9508,11 +8877,6 @@ uuid@^10.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -9523,7 +8887,7 @@ v8-compile-cache@2.3.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: +validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -9541,15 +8905,6 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - vite@4.5.5: version "4.5.5" resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.5.tgz#639b9feca5c0a3bfe3c60cb630ef28bf219d742e" @@ -9580,11 +8935,6 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -walkdir@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" - integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== - watchpack@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" @@ -9607,36 +8957,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -web-vitals@^4.2.0: +web-vitals@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7" integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw== -webdriver-js-extender@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz#57d7a93c00db4cc8d556e4d3db4b5db0a80c3bb7" - integrity sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ== - dependencies: - "@types/selenium-webdriver" "^3.0.0" - selenium-webdriver "^3.0.1" - -webdriver-manager@^12.1.7: - version "12.1.9" - resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.9.tgz#8d83543b92711b7217b39fef4cda958a4703d2df" - integrity sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ== - dependencies: - adm-zip "^0.5.2" - chalk "^1.1.1" - del "^2.2.0" - glob "^7.0.3" - ini "^1.3.4" - minimist "^1.2.0" - q "^1.4.1" - request "^2.87.0" - rimraf "^2.5.2" - semver "^5.3.0" - xml2js "^0.4.17" - webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -9893,19 +9218,6 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@^0.4.17: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" @@ -9949,7 +9261,7 @@ yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs@17.7.2, yargs@^17.2.1, yargs@^17.6.2, yargs@^17.7.2: +yargs@17.7.2, yargs@^17.2.1, yargs@^17.6.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -9998,14 +9310,9 @@ yocto-queue@^0.1.0: integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== yocto-queue@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" - integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== - -zone.js@~0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16" - integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.1.tgz#36d7c4739f775b3cbc28e6136e21aa057adec418" + integrity sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg== zone.js@~0.13.3: version "0.13.3" diff --git a/deploy/knative/cockroachdb-statefulset-single-node.yaml b/deploy/knative/cockroachdb-statefulset-single-node.yaml deleted file mode 100644 index d0f7bc12b3..0000000000 --- a/deploy/knative/cockroachdb-statefulset-single-node.yaml +++ /dev/null @@ -1,169 +0,0 @@ -# Generated file, DO NOT EDIT. Source: cloud/kubernetes/templates/cockroachdb-statefulset.yaml -apiVersion: v1 -kind: Service -metadata: - # This service is meant to be used by clients of the database. It exposes a ClusterIP that will - # automatically load balance connections to the different database pods. - name: cockroachdb-public - labels: - app: cockroachdb -spec: - ports: - # The main port, served by gRPC, serves Postgres-flavor SQL, internode - # traffic and the cli. - - port: 26257 - targetPort: 26257 - name: grpc - # The secondary port serves the UI as well as health and debug endpoints. - - port: 8080 - targetPort: 8080 - name: http - selector: - app: cockroachdb ---- -apiVersion: v1 -kind: Service -metadata: - # This service only exists to create DNS entries for each pod in the stateful - # set such that they can resolve each other's IP addresses. It does not - # create a load-balanced ClusterIP and should not be used directly by clients - # in most circumstances. - name: cockroachdb - labels: - app: cockroachdb - annotations: - # Use this annotation in addition to the actual publishNotReadyAddresses - # field below because the annotation will stop being respected soon but the - # field is broken in some versions of Kubernetes: - # https://github.com/kubernetes/kubernetes/issues/58662 - service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" - # Enable automatic monitoring of all instances when Prometheus is running in the cluster. - prometheus.io/scrape: "true" - prometheus.io/path: "_status/vars" - prometheus.io/port: "8080" -spec: - ports: - - port: 26257 - targetPort: 26257 - name: grpc - - port: 8080 - targetPort: 8080 - name: http - # We want all pods in the StatefulSet to have their addresses published for - # the sake of the other CockroachDB pods even before they're ready, since they - # have to be able to talk to each other in order to become ready. - publishNotReadyAddresses: true - clusterIP: None - selector: - app: cockroachdb ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: cockroachdb -spec: - serviceName: "cockroachdb" - replicas: 1 - selector: - matchLabels: - app: cockroachdb - template: - metadata: - labels: - app: cockroachdb - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - cockroachdb - topologyKey: kubernetes.io/hostname - containers: - - name: cockroachdb - image: cockroachdb/cockroach:latest - imagePullPolicy: IfNotPresent - # TODO: Change these to appropriate values for the hardware that you're running. You can see - # the resources that can be allocated on each of your Kubernetes nodes by running: - # kubectl describe nodes - # Note that requests and limits should have identical values. - resources: - requests: - cpu: "1" - memory: "4Gi" - limits: - cpu: "1" - memory: "4Gi" - ports: - - containerPort: 26257 - name: grpc - - containerPort: 8080 - name: http -# We recommend that you do not configure a liveness probe on a production environment, as this can impact the availability of production databases. -# livenessProbe: -# httpGet: -# path: "/health" -# port: http -# initialDelaySeconds: 30 -# periodSeconds: 5 - readinessProbe: - httpGet: - path: "/health?ready=1" - port: http - initialDelaySeconds: 10 - periodSeconds: 5 - failureThreshold: 2 - volumeMounts: - - name: datadir - mountPath: /cockroach/cockroach-data - env: - - name: COCKROACH_CHANNEL - value: kubernetes-insecure - - name: GOMAXPROCS - valueFrom: - resourceFieldRef: - resource: limits.cpu - divisor: "1" - - name: MEMORY_LIMIT_MIB - valueFrom: - resourceFieldRef: - resource: limits.memory - divisor: "1Mi" - command: - - "/bin/bash" - - "-ecx" - # The use of qualified `hostname -f` is crucial: - # Other nodes aren't able to look up the unqualified hostname. - - exec - /cockroach/cockroach - start-single-node - --logtostderr - --insecure - --advertise-host $(hostname -f) - --http-addr 0.0.0.0 - --cache $(expr $MEMORY_LIMIT_MIB / 4)MiB - --max-sql-memory $(expr $MEMORY_LIMIT_MIB / 4)MiB - # No pre-stop hook is required, a SIGTERM plus some time is all that's - # needed for graceful shutdown of a node. - terminationGracePeriodSeconds: 60 - volumes: - - name: datadir - persistentVolumeClaim: - claimName: datadir - podManagementPolicy: Parallel - updateStrategy: - type: RollingUpdate - volumeClaimTemplates: - - metadata: - name: datadir - spec: - accessModes: - - "ReadWriteOnce" - resources: - requests: - storage: 100Gi diff --git a/deploy/knative/zitadel-knative-service.yaml b/deploy/knative/zitadel-knative-service.yaml deleted file mode 100644 index 5271f99253..0000000000 --- a/deploy/knative/zitadel-knative-service.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: serving.knative.dev/v1 -kind: Service -metadata: - creationTimestamp: null - name: zitadel -spec: - template: - metadata: - annotations: - client.knative.dev/user-image: ghcr.io/zitadel/zitadel:latest - creationTimestamp: null - spec: - containerConcurrency: 0 - containers: - - args: - - admin - - start-from-init - - --masterkey - - MasterkeyNeedsToHave32Characters - env: - - name: ZITADEL_DATABASE_COCKROACH_HOST - value: cockroachdb - - name: ZITADEL_EXTERNALSECURE - value: "false" - - name: ZITADEL_TLS_ENABLED - value: "false" - - name: ZITADEL_EXTERNALPORT - value: "80" - - name: ZITADEL_EXTERNALDOMAIN - value: zitadel.default.127.0.0.1.sslip.io - image: ghcr.io/zitadel/zitadel:latest - name: user-container - ports: - - containerPort: 8080 - protocol: TCP - readinessProbe: - successThreshold: 1 - tcpSocket: - port: 0 - resources: {} - enableServiceLinks: false - timeoutSeconds: 300 diff --git a/docs/docs/apis/_v3_action_execution.proto b/docs/docs/apis/_v3_action_execution.proto deleted file mode 100644 index 80a93b1606..0000000000 --- a/docs/docs/apis/_v3_action_execution.proto +++ /dev/null @@ -1,123 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.action.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; - -message Execution { - Condition condition = 1; - // Target IDs which are called when the defined conditions trigger. - repeated string targets = 2; - // Included executions with the same condition-types. - repeated string includes = 3; -} - -message GetExecution { - zitadel.resources.object.v3alpha.Details details = 1; - Execution execution = 2; -} - -message Condition { - // Condition-types under which conditions the execution should trigger. Only one is possible. - oneof condition_type { - option (validate.required) = true; - - // Condition-type to execute after a request on the defined API point is received. - RequestExecution request = 1; - // Condition-type to execute before a response on the defined API point is returned. - ResponseExecution response = 2; - // Condition-type to execute when a function is used, replaces actions v1. - string function = 3; - // Condition-type to execute after an event is created in the system. - EventExecution event = 4; - } -} - -message RequestExecution { - // Condition for the request execution. Only one is possible. - oneof condition{ - // GRPC-method as condition. - string method = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"/zitadel.session.v2.SessionService/ListSessions\""; - } - ]; - // GRPC-service as condition. - string service = 2 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"zitadel.session.v2.SessionService\""; - } - ]; - // All calls to any available services and methods as condition. - bool all = 3; - } -} - -message ResponseExecution { - // Condition for the response execution. Only one is possible. - oneof condition{ - // GRPC-method as condition. - string method = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"/zitadel.session.v2.SessionService/ListSessions\""; - } - ]; - // GRPC-service as condition. - string service = 2 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"zitadel.session.v2.SessionService\""; - } - ]; - // All calls to any available services and methods as condition. - bool all = 3; - } -} - -message EventExecution{ - // Condition for the event execution. Only one is possible. - oneof condition{ - // Event name as condition. - string event = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"user.human.added\""; - } - ]; - // Event group as condition, all events under this group. - string group = 2 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"user.human\""; - } - ]; - // all events as condition. - bool all = 3; - } -} - diff --git a/docs/docs/apis/_v3_action_search.proto b/docs/docs/apis/_v3_action_search.proto deleted file mode 100644 index 59ead05364..0000000000 --- a/docs/docs/apis/_v3_action_search.proto +++ /dev/null @@ -1,111 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.action.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; - -import "google/api/field_behavior.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/resources/action/v3alpha/execution.proto"; - -message ExecutionSearchFilter { - oneof filter { - option (validate.required) = true; - - InConditionsFilter in_conditions = 1; - ExecutionTypeFilter execution_type = 2; - TargetFilter target = 3; - IncludeFilter include = 4; - } -} - -message InConditionsFilter { - // Defines the conditions to query for. - repeated Condition conditions = 1; -} - -message ExecutionTypeFilter { - // Defines the type to query for. - ExecutionType type = 1; -} - -message TargetFilter { - // Defines the id to query for. - string id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the targets to include" - example: "\"69629023906488334\""; - } - ]; -} - -message IncludeFilter { - // Defines the include to query for. - string include = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the include" - example: "\"request.zitadel.session.v2.SessionService\""; - } - ]; -} - -message TargetSearchFilter { - oneof query { - option (validate.required) = true; - - TargetNameFilter name = 1; - InTargetIDsFilter in_ids = 2; - } -} - -message TargetNameFilter { - // Defines the name of the target to query for. - string name = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - max_length: 200; - example: "\"ip_allow_list\""; - } - ]; - // Defines which text comparison method used for the name query. - zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ - (validate.rules).enum.defined_only = true, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "defines which text equality method is used"; - } - ]; -} - -message InTargetIDsFilter { - // Defines the ids to query for. - repeated string ids = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the ids of the targets to include" - example: "[\"69629023906488334\",\"69622366012355662\"]"; - } - ]; -} - -enum ExecutionType { - EXECUTION_TYPE_UNSPECIFIED = 0; - EXECUTION_TYPE_REQUEST = 1; - EXECUTION_TYPE_RESPONSE = 2; - EXECUTION_TYPE_EVENT = 3; - EXECUTION_TYPE_FUNCTION = 4; -} - -enum TargetFieldName { - TARGET_FIELD_NAME_UNSPECIFIED = 0; - TARGET_FIELD_NAME_ID = 1; - TARGET_FIELD_NAME_CREATION_DATE = 2; - TARGET_FIELD_NAME_CHANGE_DATE = 3; - TARGET_FIELD_NAME_NAME = 4; - TARGET_FIELD_NAME_TARGET_TYPE = 5; - TARGET_FIELD_NAME_URL = 6; - TARGET_FIELD_NAME_TIMEOUT = 7; - TARGET_FIELD_NAME_ASYNC = 8; - TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 9; -} diff --git a/docs/docs/apis/_v3_action_service.proto b/docs/docs/apis/_v3_action_service.proto deleted file mode 100644 index 1e62ddd7d7..0000000000 --- a/docs/docs/apis/_v3_action_service.proto +++ /dev/null @@ -1,554 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.action.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -import "zitadel/resources/action/v3alpha/target.proto"; -import "zitadel/resources/action/v3alpha/execution.proto"; -import "zitadel/resources/action/v3alpha/search.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Action Service"; - version: "3.0-alpha"; - description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. It is behind the feature flag \"multitenancy_resources_api\". It will continue breaking as long as it is in alpha state."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - schemes: HTTPS; - schemes: HTTP; - - consumes: "application/json"; - consumes: "application/grpc"; - - produces: "application/json"; - produces: "application/grpc"; - - consumes: "application/grpc-web+proto"; - produces: "application/grpc-web+proto"; - - host: "$ZITADEL_DOMAIN"; - base_path: "/resources/v3alpha"; - - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://zitadel.com/docs" - } - security_definitions: { - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } - security: { - security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the resource."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } -}; - -service ZITADELActions { - - // Create a target - // - // Create a new target, which can be used in executions. - rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { - option (google.api.http) = { - post: "/targets" - body: "target" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "201"; - value: { - description: "Target successfully created"; - schema: { - json_schema: { - ref: "#/definitions/v2CreateTargetResponse"; - } - } - }; - }; - }; - } - - // Patch a target - // - // Patch an existing target. - rpc PatchTarget (PatchTargetRequest) returns (PatchTargetResponse) { - option (google.api.http) = { - patch: "/targets/{id}" - body: "target" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.write" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully updated"; - }; - }; - }; - } - - // Delete a target - // - // Delete an existing target. This will remove it from any configured execution as well. - rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { - option (google.api.http) = { - delete: "/targets/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully deleted"; - }; - }; - }; - } - - // Target by ID - // - // Returns the target identified by the requested ID. - rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) { - option (google.api.http) = { - get: "/targets/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200" - value: { - description: "Target successfully retrieved"; - } - }; - }; - } - - // Search targets - // - // Search all matching targets. By default, we will return all targets of your instance. - // Make sure to include a limit and sorting for pagination. - rpc SearchTargets (SearchTargetsRequest) returns (SearchTargetsResponse) { - option (google.api.http) = { - post: "/targets/_search", - body: "filters" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all targets matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // Put an execution to call a target or include the targets of another execution. - // - // Creates an execution for the given condition if it doesn't exists. - // Otherwise, the existing execution is updated. - rpc PutExecution (PutExecutionRequest) returns (PutExecutionResponse) { - option (google.api.http) = { - post: "/executions" - body: "execution" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.execution.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "201"; - value: { - description: "Execution successfully created"; - schema: { - json_schema: { - ref: "#/definitions/v2CreateExecutionResponse"; - } - } - }; - }; - responses: { - key: "200"; - value: { - description: "Execution successfully updated"; - }; - }; - }; - } - - // Delete an execution - // - // Delete an existing execution. - rpc DeleteExecution (DeleteExecutionRequest) returns (DeleteExecutionResponse) { - option (google.api.http) = { - delete: "/executions/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.execution.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Execution successfully deleted"; - }; - }; - }; - } - - // Search executions - // - // Search all matching executions. By default, we will return all executions of your instance. - // Depending on the ZITADEL configuration, the number of returned resources is most probably limited. - // To make sure you get deterministic results, sort and paginate by the resources creation dates. - rpc SearchExecutions (SearchExecutionsRequest) returns (SearchExecutionsResponse) { - option (google.api.http) = { - post: "/executions/_search" - body: "filters" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all executions matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // List all available functions - // - // List all available functions which can be used as condition for executions. - rpc ListAvailableExecutionFunctions (ListAvailableExecutionFunctionsRequest) returns (ListAvailableExecutionFunctionsResponse) { - option (google.api.http) = { - get: "/executions/functions" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all functions successfully"; - }; - }; - }; - } - // List all available methods - // - // List all available methods which can be used as condition for executions. - rpc ListAvailableExecutionMethods (ListAvailableExecutionMethodsRequest) returns (ListAvailableExecutionMethodsResponse) { - option (google.api.http) = { - get: "/executions/methods" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all methods successfully"; - }; - }; - }; - } - // List all available service - // - // List all available services which can be used as condition for executions. - rpc ListAvailableExecutionServices (ListAvailableExecutionServicesRequest) returns (ListAvailableExecutionServicesResponse) { - option (google.api.http) = { - get: "/executions/services" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all services successfully"; - }; - }; - }; - } -} - -message CreateTargetRequest { - Target target = 2; -} - -message CreateTargetResponse { - zitadel.resources.object.v3alpha.Details details = 2; -} - -message PatchTargetRequest { - string id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - PatchTarget target = 2; -} - -message PatchTargetResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message DeleteTargetRequest { - string id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message DeleteTargetResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message SearchTargetsRequest { - // list limitations and ordering. - zitadel.resources.object.v3alpha.SearchQuery query = 2; - // the field the result is sorted. - zitadel.resources.action.v3alpha.TargetFieldName sorting_column = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"FIELD_NAME_SCHEMA_TYPE\"" - } - ]; - // Define the criteria to query for. - repeated zitadel.resources.action.v3alpha.TargetSearchFilter filters = 4; -} - -message SearchTargetsResponse { - zitadel.resources.object.v3alpha.ListDetails details = 1; - zitadel.resources.action.v3alpha.TargetFieldName sorting_column = 2; - repeated zitadel.resources.action.v3alpha.GetTarget result = 3; -} - -message GetTargetRequest { - // unique identifier of the target. - string id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message GetTargetResponse { - zitadel.resources.action.v3alpha.GetTarget target = 1; -} - -message PutExecutionRequest { - Execution execution = 2; -} - -message PutExecutionResponse { - zitadel.resources.object.v3alpha.Details details = 2; -} - -message DeleteExecutionRequest { - string id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message DeleteExecutionResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message SearchExecutionsRequest { - // list limitations and ordering. - zitadel.resources.object.v3alpha.SearchQuery query = 1; - // Define the criteria to query for. - repeated zitadel.resources.action.v3alpha.ExecutionSearchFilter filters = 2; -} - -message SearchExecutionsResponse { - zitadel.resources.object.v3alpha.ListDetails details = 1; - repeated zitadel.resources.action.v3alpha.GetExecution result = 2; -} - -message ListAvailableExecutionFunctionsRequest{} -message ListAvailableExecutionFunctionsResponse{ - // All available functions - repeated string functions = 1; -} -message ListAvailableExecutionMethodsRequest{} -message ListAvailableExecutionMethodsResponse{ - // All available methods - repeated string methods = 1; -} - -message ListAvailableExecutionServicesRequest{} -message ListAvailableExecutionServicesResponse{ - // All available services - repeated string services = 1; -} \ No newline at end of file diff --git a/docs/docs/apis/_v3_action_target.proto b/docs/docs/apis/_v3_action_target.proto deleted file mode 100644 index 217301d679..0000000000 --- a/docs/docs/apis/_v3_action_target.proto +++ /dev/null @@ -1,94 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.action.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; - -message Target { - string name = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\""; - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - SetRESTWebhook rest_webhook = 4; - SetRESTRequestResponse rest_request_response = 5; - } - // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"10s\""; - } - ]; - oneof execution_type { - // Set the execution to run asynchronously. - bool is_async = 7; - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 8; - } -} - -message GetTarget { - zitadel.resources.object.v3alpha.Details details = 1; - Target target = 2; -} - -message PatchTarget { - optional string name = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\""; - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - SetRESTWebhook rest_webhook = 3; - SetRESTRequestResponse rest_request_response = 4; - } - // Timeout defines the duration until ZITADEL cancels the execution. - optional google.protobuf.Duration timeout = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"10s\""; - } - ]; - oneof execution_type { - // Set the execution to run asynchronously. - bool is_async = 6; - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 7; - } -} - -message SetRESTWebhook { - string url = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; -} - -message SetRESTRequestResponse { - string url = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; -} diff --git a/docs/docs/apis/_v3_idp.proto b/docs/docs/apis/_v3_idp.proto deleted file mode 100644 index bb2ed741aa..0000000000 --- a/docs/docs/apis/_v3_idp.proto +++ /dev/null @@ -1,94 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.idp.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/idp/v3alpha;idp"; - -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; - -message IDP { - string name = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"GitLab\""; - } - ]; - zitadel.resources.object.v3alpha.StatePolicy state_policy = 2; - Options options = 3; -} - -message PatchIDP { - optional string name = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"GitLab\""; - } - ]; - optional zitadel.resources.object.v3alpha.StatePolicy state_policy = 2; - optional Options options = 3; -} - - -message GetIDP { - zitadel.resources.object.v3alpha.Details details = 1; - optional zitadel.resources.object.v3alpha.Parent parent = 2; - zitadel.resources.object.v3alpha.State state = 3; - ProviderType type = 4; - IDP idp = 5; -} - -enum ProviderType { - PROVIDER_TYPE_UNSPECIFIED = 0; - PROVIDER_TYPE_OIDC = 1; - PROVIDER_TYPE_JWT = 2; - PROVIDER_TYPE_LDAP = 3; - PROVIDER_TYPE_OAUTH = 4; - PROVIDER_TYPE_AZURE_AD = 5; - PROVIDER_TYPE_GITHUB = 6; - PROVIDER_TYPE_GITHUB_ES = 7; - PROVIDER_TYPE_GITLAB = 8; - PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9; - PROVIDER_TYPE_GOOGLE = 10; - PROVIDER_TYPE_APPLE = 11; - PROVIDER_TYPE_SAML = 12; -} - - -enum AutoLinkingOption { - // AUTO_LINKING_OPTION_UNSPECIFIED disables the auto linking prompt. - AUTO_LINKING_OPTION_UNSPECIFIED = 0; - // AUTO_LINKING_OPTION_USERNAME will use the username of the external user to check for a corresponding ZITADEL user. - AUTO_LINKING_OPTION_USERNAME = 1; - // AUTO_LINKING_OPTION_EMAIL will use the email of the external user to check for a corresponding ZITADEL user with the same verified email - // Note that in case multiple users match, no prompt will be shown. - AUTO_LINKING_OPTION_EMAIL = 2; -} - -message Options { - bool is_manual_linking_allowed = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if users should be able to link an existing ZITADEL user with an external account. Disable if users should only be allowed to link the proposed account in case of active auto_linking."; - } - ]; - bool is_manual_creation_allowed = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if users should be able to create a new account in ZITADEL when using an external account. Disable if users should not be able to edit account information when auto_creation is enabled."; - } - ]; - bool is_auto_creation = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if a new account in ZITADEL should be created automatically when login with an external account."; - } - ]; - bool is_auto_update = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if a the ZITADEL account fields should be updated automatically on each login."; - } - ]; - AutoLinkingOption auto_linking = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if users should get prompted to link an existing ZITADEL user to an external account if the selected attribute matches."; - } - ]; -} diff --git a/docs/docs/apis/_v3_idp_gitlab.proto b/docs/docs/apis/_v3_idp_gitlab.proto deleted file mode 100644 index 9c7ed47a78..0000000000 --- a/docs/docs/apis/_v3_idp_gitlab.proto +++ /dev/null @@ -1,59 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.idp.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/idp/v3alpha;idp"; - -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/resources/idp/v3alpha/idp.proto"; - -message GetGitLabIDP { - zitadel.resources.object.v3alpha.Details details = 1; - optional zitadel.resources.object.v3alpha.Parent parent = 2; - zitadel.resources.object.v3alpha.State state = 3; - ProviderType type = 4; - GitLabIDP idp = 5; -} - -message GitLabIDP { - IDP idp = 1; - GitLabConfig config = 2; -} - -message PatchGitLabIDP { - optional PatchIDP idp = 1; - optional PatchGitLabConfig config = 2; -} - -message GitLabConfig { - string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - description: "client id of the GitLab application"; - } - ]; - repeated string scopes = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - description: "the scopes requested by ZITADEL during the request to GitLab"; - } - ]; -} - -message PatchGitLabConfig { - optional string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - description: "client id of the GitLab application"; - } - ]; - repeated string scopes = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - description: "the scopes requested by ZITADEL during the request to GitLab"; - } - ]; -} \ No newline at end of file diff --git a/docs/docs/apis/_v3_idp_search.proto b/docs/docs/apis/_v3_idp_search.proto deleted file mode 100644 index dbc75e2d20..0000000000 --- a/docs/docs/apis/_v3_idp_search.proto +++ /dev/null @@ -1,47 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.idp.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/idp/v3alpha;idp"; - -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; - -enum IDPFieldName { - IDP_FIELD_NAME_UNSPECIFIED = 0; - IDP_FIELD_NAME_NAME = 1; -} - -message IDPSearchFilter { - oneof filter { - IDPIDFilter id = 1; - IDPNameFilter name = 2; - resources.object.v3alpha.StateFilter state = 3; - } -} - -message IDPIDFilter { - string id = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\""; - } - ]; -} - -message IDPNameFilter { - string name = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"google\""; - } - ]; - zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ - (validate.rules).enum.defined_only = true, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "defines which text equality method is used"; - } - ]; -} diff --git a/docs/docs/apis/_v3_idp_service.proto b/docs/docs/apis/_v3_idp_service.proto deleted file mode 100644 index 26f734f68d..0000000000 --- a/docs/docs/apis/_v3_idp_service.proto +++ /dev/null @@ -1,325 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.idp.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/resources/idp/v3alpha/search.proto"; -import "zitadel/resources/idp/v3alpha/idp.proto"; -import "zitadel/resources/idp/v3alpha/gitlab.proto"; -import "zitadel/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/idp/v3alpha;idp"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Identity Provider Service"; - version: "3.0-alpha"; - description: "This API is intended to manage identity providers (IDPs). IDPs can be created for specific organizations or for an instance. IDPs created on an instance can be activated (reused) or deactivated in organizations. It is behind the feature flag \"multitenancy_resources_api\". It will continue breaking as long as it is in alpha state."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - schemes: HTTPS; - schemes: HTTP; - - consumes: "application/json"; - consumes: "application/grpc"; - - produces: "application/json"; - produces: "application/grpc"; - - consumes: "application/grpc-web+proto"; - produces: "application/grpc-web+proto"; - - host: "$ZITADEL_DOMAIN"; - base_path: "/resources/v3alpha"; - - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://zitadel.com/docs" - } - security_definitions: { - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } - security: { - security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the resource."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } -}; - -service ZITADELIdentityProviders { - - // Create a GitLab IDP - rpc CreateGitLabIDP (CreateGitLabIDPRequest) returns (CreateGitLabIDPResponse) { - option (google.api.http) = { - post: "/idps/gitlab" - body: "idp" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "idp.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "201"; - value: { - description: "GitLabIDP successfully created"; - schema: { - json_schema: { - ref: "#/definitions/v2CreateGitLabIDPResponse"; - } - } - }; - }; - }; - } - - // Patch a GitLab IDP - rpc PatchGitLabIDP (PatchGitLabIDPRequest) returns (PatchGitLabIDPResponse) { - option (google.api.http) = { - patch: "/idps/gitlab/{id}" - body: "idp" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "idp.write" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "GitLabIDP successfully updated"; - }; - }; - }; - } - - // Find a GitLab IDP by ID - rpc GetGitLabIDP (GetGitLabIDPRequest) returns (GetGitLabIDPResponse) { - option (google.api.http) = { - get: "/idps/gitlab/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "idp.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200" - value: { - description: "GitLabIDP successfully retrieved"; - } - }; - }; - } - - // Delete an IDP of any type - rpc DeleteIDP (DeleteIDPRequest) returns (DeleteIDPResponse) { - option (google.api.http) = { - delete: "/idps/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "idp.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Identity provider successfully deleted"; - }; - }; - }; - } - - // Search IDPs - // - // Search all matching IDPs. By default, all instance-level and organization-level providers of all types are returned. - // Only type-agnostic properties are returned in the response. - // To get the full details of a specific IDP, use the specific types Get method. - // If you search by passing an organization context, the state and the state policy might be different than if you search within the default instance-level context. - // Make sure to include a limit and sorting for pagination. - rpc SearchIDPs (SearchIDPsRequest) returns (SearchIDPsResponse) { - option (google.api.http) = { - post: "/idps/_search", - body: "filters" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "idp.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all IDPs matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } -} - -message CreateGitLabIDPRequest { - optional zitadel.object.v3alpha.RequestContext ctx = 1; - GitLabIDP idp = 2; -} - -message CreateGitLabIDPResponse { - zitadel.resources.object.v3alpha.Details details = 2; -} - -message PatchGitLabIDPRequest { - string id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - PatchGitLabIDP idp = 2; -} - -message PatchGitLabIDPResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message DeleteIDPRequest { - string id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message DeleteIDPResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message SearchIDPsRequest { - optional zitadel.object.v3alpha.RequestContext ctx = 1; - // list limitations and ordering. - zitadel.resources.object.v3alpha.SearchQuery query = 2; - // the field the result is sorted. - zitadel.resources.idp.v3alpha.IDPFieldName sorting_column = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"FIELD_NAME_SCHEMA_TYPE\"" - } - ]; - repeated zitadel.resources.idp.v3alpha.IDPSearchFilter filters = 4; -} - -message SearchIDPsResponse { - zitadel.resources.object.v3alpha.ListDetails details = 1; - zitadel.resources.idp.v3alpha.IDPFieldName sorting_column = 2; - repeated zitadel.resources.idp.v3alpha.GetIDP result = 3; -} - -message GetGitLabIDPRequest { - string id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message GetGitLabIDPResponse { - zitadel.resources.idp.v3alpha.GetGitLabIDP idp = 1; -} diff --git a/docs/docs/apis/_v3_language.proto b/docs/docs/apis/_v3_language.proto deleted file mode 100644 index 269bb4b0a0..0000000000 --- a/docs/docs/apis/_v3_language.proto +++ /dev/null @@ -1,69 +0,0 @@ -syntax = "proto3"; - -package zitadel.settings.language.v3alpha; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/language/v3alpha;language"; - -import "validate/validate.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; - -import "zitadel/settings/object/v3alpha/object.proto"; -import "zitadel/object/v3alpha/object.proto"; - -message SetLanguageSettings { - optional zitadel.settings.object.v3alpha.Language default_language = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "default language for the current context" - example: "\"en\"" - } - ]; - optional SetLanguages restricted_languages = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "To these languages, message texts and default login UI labels are translated to. Also, the discovery endpoint only lists these languages." - example: "[\"en\", \"de\"]" - } - ]; -} - -message ResolvedLanguageSettings { - zitadel.settings.object.v3alpha.ResolvedString default_language = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "default language for the current context" - example: "\"en\"" - } - ]; - ResolvedLanguages restricted_languages = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "To these languages, message texts and default login UI labels are translated to. Also, the discovery endpoint only lists these languages." - example: "[\"en\", \"de\"]" - } - ]; - ResolvedLanguages supported_languages = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "These languages are supported by the system. For simplicity, the field is of type ResolvedLanguages, even though the list is immutable and the owner is always of OWNER_TYPE_SYSTEM." - example: "[\"en\", \"de\", \"it\"]" - } - ]; -} - -message SetLanguages { - repeated zitadel.settings.object.v3alpha.Language languages = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "List of languages to set" - example: "[\"en\", \"de\"]" - } - ]; -} - -message ResolvedLanguages { - repeated zitadel.settings.object.v3alpha.Language value = 1[ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "List of languages" - example: "[\"en\", \"de\"]" - } - ]; - optional zitadel.object.v3alpha.Owner owner = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "If the value is inherited, the value is inherited from this owner."; - } - ]; -} diff --git a/docs/docs/apis/_v3_language_service.proto b/docs/docs/apis/_v3_language_service.proto deleted file mode 100644 index f282aa2f9b..0000000000 --- a/docs/docs/apis/_v3_language_service.proto +++ /dev/null @@ -1,158 +0,0 @@ -syntax = "proto3"; - -package zitadel.settings.language.v3alpha; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/language/v3alpha;language"; - -import "google/api/annotations.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; - -import "zitadel/object/v3alpha/object.proto"; -import "zitadel/settings/object/v3alpha/object.proto"; -import "zitadel/settings/language/v3alpha/language.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Language Settings Service"; - version: "3.0-alpha"; - description: "Language Service is intended to manage languages for ZITADEL. Enable the feature flag \"multitenancy_settings\" in order to activate it. Languages are settings, and are therefore inherited through the context hierarchy system -> instance -> org. It will continue breaking as long as it is in alpha state."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - schemes: HTTPS; - schemes: HTTP; - - consumes: "application/json"; - consumes: "application/grpc"; - consumes: "application/grpc-web+proto"; - - produces: "application/json"; - produces: "application/grpc"; - produces: "application/grpc-web+proto"; - - host: "$ZITADEL_DOMAIN"; - base_path: "/settings/v3alpha"; - - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://zitadel.com/docs" - } - security_definitions: { - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$ZITADEL_DOMAIN/oauth/v2/authorize"; - token_url: "$ZITADEL_DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } - security: { - security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the settings in the given context."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } -}; - -// ZITADELLanguageSettings is intended to manage languages for ZITADEL. -// Enable the feature flag \"multitenancy_settings\" in order to activate it. -// Languages are settings, and are therefore inherited through the context hierarchy system -> instance -> org. -service ZITADELLanguageSettings { - rpc SetLanguages (SetLanguageSettingsRequest) returns (SetLanguageSettingsResponse) { - option (google.api.http) = { - patch: "/languages" - body: "settings" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "authenticated" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set languages for a given context"; - description: "Configure and set languages for a given context. Only fields present in the request are set or unset." - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - }; - - rpc ResolveLanguages (ResolveLanguageSettingsRequest) returns (ResolveLanguageSettingsResponse) { - option (google.api.http) = { - get: "/languages" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "authenticated" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get the languages in the given context"; - description: "Returns all configured and inherited languages for the given context." - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - }; -} - -message SetLanguageSettingsRequest{ - optional zitadel.object.v3alpha.RequestContext ctx = 1; - SetLanguageSettings settings = 2; -} - -message SetLanguageSettingsResponse{ - zitadel.settings.object.v3alpha.Details details = 1; -} - -message ResolveLanguageSettingsRequest{ - optional zitadel.object.v3alpha.RequestContext ctx = 1; -} - -message ResolveLanguageSettingsResponse{ - zitadel.settings.object.v3alpha.Details details = 1; - ResolvedLanguageSettings settings = 2; -} diff --git a/docs/docs/apis/_v3_object.proto b/docs/docs/apis/_v3_object.proto deleted file mode 100644 index dc018c4ee7..0000000000 --- a/docs/docs/apis/_v3_object.proto +++ /dev/null @@ -1,33 +0,0 @@ -syntax = "proto3"; - -package zitadel.object.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha;object"; - -import "google/protobuf/timestamp.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -message RequestContext { - // By default, the request context is set to the instance discovered by the domain from the requests host header. - oneof owner { - bool system = 1 [(validate.rules).bool = {const: true}]; // TODO: move the source of truth from the defaults.yaml into the database - string instance_id = 2; - string instance_domain = 3; - string org_id = 4; - string org_domain = 5; - } -} - -enum OwnerType { - OWNER_TYPE_UNSPECIFIED = 0; - OWNER_TYPE_SYSTEM = 1; // TODO: move the source of truth from the defaults.yaml into the database - OWNER_TYPE_INSTANCE = 2; - OWNER_TYPE_ORG = 3; -} - -message Owner { - OwnerType type = 1; - string id = 2; -} - diff --git a/docs/docs/apis/_v3_resource_object.proto b/docs/docs/apis/_v3_resource_object.proto deleted file mode 100644 index 7bd5e60a9b..0000000000 --- a/docs/docs/apis/_v3_resource_object.proto +++ /dev/null @@ -1,155 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.object.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha;object"; - -import "google/api/field_behavior.proto"; -import "google/protobuf/timestamp.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/object/v3alpha/object.proto"; - -message Organization { - oneof org { - string org_id = 1; - string org_domain = 2; - } -} - -message Details { - string id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629012906488334\""; - } - ]; - - //sequence represents the order of events. It's always counting - // - // on read: the sequence of the last event reduced by the projection - // - // on manipulation: the timestamp of the event(s) added by the manipulation - uint64 sequence = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"2\""; - } - ]; - //change_date is the timestamp when the object was changed - // - // on read: the timestamp of the last event reduced by the projection - // - // on manipulation: the timestamp of the event(s) added by the manipulation - google.protobuf.Timestamp change_date = 3; - //resource_owner represents the context an object belongs to - zitadel.object.v3alpha.Owner resource_owner = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\""; - } - ]; -} - -enum State { - EFFECTIVE_STATE_UNSPECIFIED = 0; - EFFECTIVE_STATE_ACTIVE = 1; - EFFECTIVE_STATE_INACTIVE = 2; -} - -enum StatePolicy { - STATE_POLICY_UNSPECIFIED = 0; - STATE_POLICY_ACTIVATE = 1; - STATE_POLICY_DEACTIVATE = 2; - STATE_POLICY_INHERIT = 3; -} - -message StateFilter { - // Defines the state to query for. - resources.object.v3alpha.State state = 1 [ - (validate.rules).enum.defined_only = true, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"STATE_ACTIVE\"" - } - ]; -} - -message Parent { - zitadel.object.v3alpha.Owner parent = 1; - State state = 2; -} - -message ListQuery { - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - title: "General List Query" - description: "Object unspecific list filters like offset, limit and asc/desc." - } - }; - uint64 offset = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"0\""; - } - ]; - uint32 limit = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "100"; - description: "Maximum amount of events returned. The default is 100, the maximum is 1000. If the limit exceeds the maximum, ZITADEL throws an error."; - } - ]; - bool asc = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "default is descending" - } - ]; -} - -message ListDetails { - uint32 applied_limit = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "100"; - } - ]; - bool end_of_list = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - } - ]; - uint64 total_result = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"2\""; - } - ]; - uint64 processed_sequence = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"267831\""; - } - ]; - google.protobuf.Timestamp timestamp = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the last time the projection got updated" - } - ]; -} - -enum TextFilterMethod { - TEXT_FILTER_METHOD_EQUALS = 0; - TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE = 1; - TEXT_FILTER_METHOD_STARTS_WITH = 2; - TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE = 3; - TEXT_FILTER_METHOD_CONTAINS = 4; - TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE = 5; - TEXT_FILTER_METHOD_ENDS_WITH = 6; - TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE = 7; -} - -enum ListFilterMethod { - LIST_FILTER_METHOD_IN = 0; -} - -enum TimestampFilterMethod { - TIMESTAMP_Filter_METHOD_EQUALS = 0; - TIMESTAMP_Filter_METHOD_GREATER = 1; - TIMESTAMP_Filter_METHOD_GREATER_OR_EQUALS = 2; - TIMESTAMP_Filter_METHOD_LESS = 3; - TIMESTAMP_Filter_METHOD_LESS_OR_EQUALS = 4; -} \ No newline at end of file diff --git a/docs/docs/apis/_v3_settings_object.proto b/docs/docs/apis/_v3_settings_object.proto deleted file mode 100644 index 5a1fab69a8..0000000000 --- a/docs/docs/apis/_v3_settings_object.proto +++ /dev/null @@ -1,193 +0,0 @@ -syntax = "proto3"; - -package zitadel.settings.object.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha;object"; - -import "google/protobuf/timestamp.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "google/protobuf/duration.proto"; - -import "zitadel/object/v3alpha/object.proto"; - -message Details { - //sequence represents the order of events. It's always counting - // - // on read: the sequence of the last event reduced by the projection - // - // on manipulation: the timestamp of the event(s) added by the manipulation - uint64 sequence = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"2\""; - } - ]; - //change_date is the timestamp when the object was changed - // - // on read: the timestamp of the last event reduced by the projection - // - // on manipulation: the timestamp of the event(s) added by the manipulation - google.protobuf.Timestamp change_date = 2; - //resource_owner represents the context an object belongs to - zitadel.object.v3alpha.Owner owner = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\""; - } - ]; -} - - -message ResolvedBool { - bool value = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "false"; - description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context."; - } - ]; - optional zitadel.object.v3alpha.Owner owner = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "If the value is inherited, the value is inherited from this owner."; - } - ]; -} - -message SetBool { - oneof value { - bool set = 1; - bool reset = 2 [(validate.rules).bool = { - const: true - }]; - } -} - -message ResolvedString { - string value = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"a resolved string\""; - description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context."; - } - ]; - optional zitadel.object.v3alpha.Owner owner = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "If the value is inherited, the value is inherited from this owner."; - } - ]; -} - -message SetString { - oneof value { - string set = 1 [ - (validate.rules).string = { - max_len: 256 - } - ]; - bool reset = 2 [(validate.rules).bool = { - const: true - }]; - } -} - -message SetStringLong { - oneof value { - string set = 1 [ - (validate.rules).string = { - max_len: 2048 - } - ]; - bool reset = 2 [(validate.rules).bool = { - const: true - }]; - } -} - - -message SetStringShort { - oneof value { - string set = 1 [ - (validate.rules).string = { - max_len: 64 - } - ]; - bool reset = 2 [(validate.rules).bool = { - const: true - }]; - } -} - -message Language { - string key = 1 [(validate.rules).string = {pattern: "^[a-z]{2}$"}]; -} - -message ResolvedStrings { - repeated string value = 1[ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"a\", \"resolved\", \"list\", \"of\", \"strings\"]"; - description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context."; - } - ]; - optional zitadel.object.v3alpha.Owner owner = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "If the value is inherited, the value is inherited from this owner."; - } - ]; -} - -message SetStrings { - oneof value { - SetStringsValue set = 1; - bool reset = 2 [(validate.rules).bool = { - const: true - }]; - } -} - -message SetStringsValue { - repeated string value = 1; -} - -message ResolvedUInt64 { - uint64 value = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "1000"; - description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context."; - } - ]; - optional zitadel.object.v3alpha.Owner owner = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "If the value is inherited, the value is inherited from this owner."; - } - ]; -} - -message SetUInt64 { - oneof value { - uint64 set = 1; - bool reset = 2 [(validate.rules).bool = { - const: true - }]; - } -} - - -message ResolvedDuration { - google.protobuf.Duration value = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"5s\""; - description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context."; - } - ]; - optional zitadel.object.v3alpha.Owner owner = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "If the value is inherited, the value is inherited from this owner."; - } - ]; -} - -message SetDuration { - oneof value { - google.protobuf.Duration set = 1; - bool reset = 2 [(validate.rules).bool = { - const: true - }]; - } -} diff --git a/docs/docs/apis/actions/external-authentication.md b/docs/docs/apis/actions/external-authentication.md index 6a3a4d5551..114185871b 100644 --- a/docs/docs/apis/actions/external-authentication.md +++ b/docs/docs/apis/actions/external-authentication.md @@ -18,6 +18,8 @@ The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_POST_A The first parameter contains the following fields - `accessToken` *string* The access token returned by the identity provider. This can be an opaque token or a JWT + - `refreshToken` *string* + The refresh token returned by the identity provider if there is one. This is most likely to be an opaque token. - `claimsJSON()` [*idTokenClaims*](../openidoauth/claims) Returns all claims of the id token - `getClaim(key)` *Any* diff --git a/docs/docs/apis/actions/v3/usage.md b/docs/docs/apis/actions/v3/usage.md deleted file mode 100644 index 2e89f3ce36..0000000000 --- a/docs/docs/apis/actions/v3/usage.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: Using Actions ---- - -The Action API provides a flexible mechanism for customizing and extending the functionality of ZITADEL. By allowing you to define targets and executions, you can implement custom workflows triggered on an API requests and responses, events or specific functions. - -**How it works:** -- Create Target -- Set Execution with condition and target -- Custom Code will be triggered and executed - -**Use Cases:** -- User Management: Automate provisioning user data to external systems when users are crreated, updated or deleted. -- Security: Implement IP blocking or rate limiting based on API usage patterns. -- Extend Workflows: Automatically setup resources in your application, when a new organization in ZITADEL is created. -- Token extension: Add custom claims to the tokens. - -## Endpoints - -ZITADEL sends an HTTP Post request to the endpoint set as Target, the received request than can be edited and send back or custom processes can be handled. - -### Sent information Request - -The information sent to the Endpoint is structured as JSON: - -```json -{ - "fullMethod": "full method of the GRPC call", - "instanceID": "instanceID of the called instance", - "orgID": "ID of the organization related to the calling context", - "projectID": "ID of the project related to the used application", - "userID": "ID of the calling user", - "request": "full request of the call" -} -``` - -### Sent information Response - -The information sent to the Endpoint is structured as JSON: - -```json -{ - "fullMethod": "full method of the GRPC call", - "instanceID": "instanceID of the called instance", - "orgID": "ID of the organization related to the calling context", - "projectID": "ID of the project related to the used application", - "userID": "ID of the calling user", - "request": "full request of the call", - "response": "full response of the call" -} -``` - -## Target - -The Target describes how ZITADEL interacts with the Endpoint. - -There are different types of Targets: - -- `Webhook`, the call handles the status code but response is irrelevant, can be InterruptOnError -- `Call`, the call handles the status code and response, can be InterruptOnError -- `Async`, the call handles neither status code nor response, but can be called in parallel with other Targets - -`InterruptOnError` means that the Execution gets interrupted if any of the calls return with a status code >= 400, and the next Target will not be called anymore. - -The API documentation to create a target can be found [here](/apis/resources/action_service_v3/zitadel-actions-create-target) - -### Content Signing - -To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical. - -Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v3/zitadel-actions-create-target), -and can also be newly generated when a Target is [patched](/apis/resources/action_service_v3/zitadel-actions-patch-target). - -## Execution - -ZITADEL decides on specific conditions if one or more Targets have to be called. -The Execution resource contains 2 parts, the condition and the called targets. - -The condition can be defined for 4 types of processes: - -- `Requests`, before a request is processed by ZITADEL -- `Responses`, before a response is sent back to the application -- `Functions`, handling specific functionality in the logic of ZITADEL -- `Events`, after a specific event happened and was stored in ZITADEL - -The API documentation to set an Execution can be found [here](/apis/resources/action_service_v3/zitadel-actions-set-execution) - -### Condition Best Match - -As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match. -This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2.UserService` and on `/zitadel.user.v2.UserService/AddHumanUser`, -ZITADEL would with a call on the `/zitadel.user.v2.UserService/AddHumanUser` use the Executions with the following priority: - -1. `/zitadel.user.v2.UserService/AddHumanUser` -2. `zitadel.user.v2.UserService` -3. `all` - -If you then have a call on `/zitadel.user.v2.UserService/UpdateHumanUser` the following priority would be found: - -1. `zitadel.user.v2.UserService` -2. `all` - -And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used. - -### Targets and Includes - -:::info -Includes are limited to 3 levels, which mean that include1->include2->include3 is the maximum for now. -If you have feedback to the include logic, or a reason why 3 levels are not enough, please open [an issue on github](https://github.com/zitadel/zitadel/issues) or [start a discussion on github](https://github.com/zitadel/zitadel/discussions)/[start a topic on discord](https://zitadel.com/chat) -::: - -An execution can not only contain a list of Targets, but also Includes. -The Includes can be defined in the Execution directly, which means you include all defined Targets by a before set Execution. - -If you define 2 Executions as follows: - -```json -{ - "condition": { - "request": { - "service": "zitadel.user.v2.UserService" - } - }, - "targets": [ - { - "target": "" - } - ] -} -``` - -```json -{ - "condition": { - "request": { - "method": "/zitadel.user.v2.UserService/AddHumanUser" - } - }, - "targets": [ - { - "target": "" - }, - { - "include": { - "request": { - "service": "zitadel.user.v2.UserService" - } - } - } - ] -} -``` - -The called Targets on "/zitadel.user.v2.UserService/AddHumanUser" would be, in order: - -1. `` -2. `` - -### Condition for Requests and Responses - -For Request and Response there are 3 levels the condition can be defined: - -- `Method`, handling a request or response of a specific GRPC full method, which includes the service name and method of the ZITADEL API -- `Service`, handling any request or response under a service of the ZITADEL API -- `All`, handling any request or response under the ZITADEL API - -The available conditions can be found under: -- [All available Methods](/apis/resources/action_service_v3/zitadel-actions-list-execution-methods), for example `/zitadel.user.v2.UserService/AddHumanUser` -- [All available Services](/apis/resources/action_service_v3/zitadel-actions-list-execution-services), for example `zitadel.user.v2.UserService` - -### Condition for Functions - -Replace the current Actions with the following flows: - -- [Internal Authentication](/apis/actions/internal-authentication) -- [External Authentication](/apis/actions/external-authentication) -- [Complement Token](/apis/actions/complement-token) -- [Customize SAML Response](/apis/actions/customize-samlresponse) - -The available conditions can be found under [all available Functions](/apis/resources/action_service_v3/zitadel-actions-list-execution-functions). - -### Condition for Events - -For event there are 3 levels the condition can be defined: - -- Event, handling a specific event -- Group, handling a specific group of events -- All, handling any event in ZITADEL - -The concept of events can be found under [Events](/concepts/architecture/software#events) - -### Error forwarding - -If you want to forward a specific error from the Target through ZITADEL, you can provide a response from the Target with status code 200 and a JSON in the following format: - -```json -{ - "forwardedStatusCode": 403, - "forwardedErrorMessage": "Call is forbidden through the IP AllowList definition" -} -``` - -Only values from 400 to 499 will be forwarded through ZITADEL, other StatusCodes will end in a PreconditionFailed error. - -If the Target returns any other status code than >= 200 and < 299, the execution is looked at as failed, and a PreconditionFailed error is logged. diff --git a/docs/docs/apis/benchmarks/_template.mdx b/docs/docs/apis/benchmarks/_template.mdx index a6a9109309..f015d20768 100644 --- a/docs/docs/apis/benchmarks/_template.mdx +++ b/docs/docs/apis/benchmarks/_template.mdx @@ -12,9 +12,11 @@ copy (SELECT , approx_quantile(metric_value, 0.95) AS p95 , approx_quantile(metric_value, 0.99) AS p99 FROM - read_csv('/path/to/k6-output.csv', auto_detect=false, delim=',', quote='"', escape='"', new_line='\n', skip=0, comment='', header=true, columns={'metric_name': 'VARCHAR', 'timestamp': 'BIGINT', 'metric_value': 'DOUBLE', 'check': 'VARCHAR', 'error': 'VARCHAR', 'error_code': 'VARCHAR', 'expected_response': 'BOOLEAN', 'group': 'VARCHAR', 'method': 'VARCHAR', 'name': 'VARCHAR', 'proto': 'VARCHAR', 'scenario': 'VARCHAR', 'service': 'VARCHAR', 'status': 'BIGINT', 'subproto': 'VARCHAR', 'tls_version': 'VARCHAR', 'url': 'VARCHAR', 'extra_tags': 'VARCHAR', 'metadata': 'VARCHAR'}) + read_csv('result.csv', auto_detect=false, null_padding=true, delim=',', quote='"', escape='"', new_line='\n', skip=0, comment='', header=true, columns={'metric_name': 'VARCHAR', 'timestamp': 'BIGINT', 'metric_value': 'DOUBLE', 'check': 'VARCHAR', 'error': 'VARCHAR', 'error_code': 'VARCHAR', 'expected_response': 'BOOLEAN', 'group': 'VARCHAR', 'method': 'VARCHAR', 'name': 'VARCHAR', 'proto': 'VARCHAR', 'scenario': 'VARCHAR', 'service': 'VARCHAR', 'status': 'BIGINT', 'subproto': 'VARCHAR', 'tls_version': 'VARCHAR', 'url': 'VARCHAR', 'extra_tags': 'VARCHAR', 'metadata': 'VARCHAR'}) WHERE - metric_name LIKE '%_duration' + metric_name <> 'http_req_duration' + AND metric_name <> 'iteration_duration' + AND metric_name LIKE '%_duration' GROUP BY metric_name , timestamp @@ -26,8 +28,6 @@ copy (SELECT --> -## Summary - TODO: describe the outcome of the test? ## Performance test results @@ -48,7 +48,7 @@ TODO: describe the outcome of the test? | ZITADEL Version | | | ZITADEL Configuration | | | ZITADEL feature flags | | -| Database | type: crdb / psql
version: | +| Database | type: psql
version: | | Database location | | | Database specification | vCPU:
memory: Gb | | ZITADEL metrics during test | | diff --git a/docs/docs/apis/benchmarks/index.mdx b/docs/docs/apis/benchmarks/index.mdx index e5d89dbae8..a0979f0081 100644 --- a/docs/docs/apis/benchmarks/index.mdx +++ b/docs/docs/apis/benchmarks/index.mdx @@ -57,9 +57,9 @@ The following metrics must be collected for each test iteration. The metrics are | ZITADEL Version | Setup | The version of zitadel deployed | Semantic version or commit | | ZITADEL Configuration | Setup | Configuration of zitadel which deviates from the defaults and is not secret | yaml | | ZITADEL feature flags | Setup | Changed feature flags | yaml | -| Database | Setup | Database type and version | **type**: crdb / psql **version**: semantic version | +| Database | Setup | Database type and version | **type**: psql **version**: semantic version | | Database location | Setup | Region or location of the deployment of the database. If not further specified the hoster is Google Cloud SQL | Location / Region | -| Database specification | Setup | The description must at least clarify the following metrics: vCPU, Memory and egress bandwidth (Scale) | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps **scale**: Amount of crdb nodes if crdb is used | +| Database specification | Setup | The description must at least clarify the following metrics: vCPU, Memory and egress bandwidth (Scale) | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps | | ZITADEL metrics during test | Result | This metric helps understanding the bottlenecks of the executed test. At least the following metrics must be provided: CPU usage Memory usage | **CPU usage** in percent **Memory usage** in percent | | Observed errors | Result | Errors worth mentioning, mostly unexpected errors | description | | Top 3 most expensive database queries | Result | The execution plan of the top 3 most expensive database queries during the test execution | database execution plan | diff --git a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx index f2e198c510..4c2809feb4 100644 --- a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx @@ -3,8 +3,6 @@ title: machine jwt profile grant benchmark of zitadel v2.65.0 sidebar_label: machine jwt profile grant --- -## Summary - Tests are halted after this test run because of too many [client read events](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/wait-event.clientread.html) on the database. ## Performance test results diff --git a/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx index 50dd7dc7ec..881e7a38ee 100644 --- a/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx @@ -3,8 +3,6 @@ title: machine jwt profile grant benchmark of zitadel v2.66.0 sidebar_label: machine jwt profile grant --- -## Summary - The tests showed heavy database load by time by the first two database queries. These queries need to be analyzed further. ## Performance test results diff --git a/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx new file mode 100644 index 0000000000..fa8c84bc7e --- /dev/null +++ b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx @@ -0,0 +1,102 @@ +--- +title: machine jwt profile grant benchmark of zitadel v2.70.0 +sidebar_label: machine jwt profile grant +--- + +The performance goals of [this issue](https://github.com/zitadel/zitadel/issues/8352) are reached. Next we will test linear scalability. + +## Performance test results + +| Metric | Value | +|:--------------------------------------|:------| +| Baseline | none | +| Purpose | Test current performance | +| Test start | 09:48 UTC | +| Test duration | 30min | +| Executed test | machine\_jwt\_profile\_grant | +| k6 version | v0.57.0 | +| VUs | 600 | +| Client location | US1 | +| ZITADEL location | US1 | +| ZITADEL container specification | vCPU: 6
Memory: 6 Gi
Container min scale: 2
Container max scale: 7 | +| ZITADEL Version | v2.70.0 | +| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] | +| Database | type: psql
version: v17.2 | +| Database location | US1 | +| Database specification | vCPU: 8
memory: 32Gib | +| ZITADEL metrics during test | | +| Observed errors | | +| Top 3 most expensive database queries | 1: Write events using eventstore.push function
2: Query user
3: Query events by instance id, aggregate type, aggregate id, event types, owner
| +| k6 Iterations per second | 1806 | +| k6 output | [output](#k6-output) | +| flowchart outcome | Scale out | + +## Endpoint latencies + +import OutputSource from "!!raw-loader!./output.json"; + +import { BenchmarkChart } from '/src/components/benchmark_chart'; + + + +## k6 output {#k6-output} + +```bash + ✓ openid configuration + ✗ token status ok + ↳ 99% — ✓ 3289559 / ✗ 5 + ✗ access token returned + ↳ 99% — ✓ 3289559 / ✗ 5 + + █ setup + + ✓ user defined + ✓ authorize status ok + ✓ login name status ok + ✓ login shows password page + ✓ password status ok + ✓ password callback + ✓ code set + ✓ token status ok + ✓ access token created + ✓ id token created + ✓ info created + ✓ org created + ✓ create user is status ok + ✓ generate machine key status ok + + █ teardown + + ✓ org removed + + checks...............................: 99.99% 6580931 out of 6580941 + data_received........................: 4.8 GB 2.6 MB/s + data_sent............................: 2.8 GB 1.5 MB/s + http_req_blocked.....................: min=110ns avg=53.66µs max=937.62ms p(50)=420ns p(95)=660ns p(99)=989ns + http_req_connecting..................: min=0s avg=22.26µs max=532.7ms p(50)=0s p(95)=0s p(99)=0s + http_req_duration....................: min=16.42ms avg=323.71ms max=3.4s p(50)=209.85ms p(95)=903.86ms p(99)=1.01s + { expected_response:true }.........: min=16.42ms avg=323.71ms max=3.4s p(50)=209.85ms p(95)=903.85ms p(99)=1.01s + http_req_failed......................: 0.00% 5 out of 3291974 + http_req_receiving...................: min=18.23µs avg=1.73ms max=913.21ms p(50)=73.15µs p(95)=8.73ms p(99)=29.35ms + http_req_sending.....................: min=17.54µs avg=53.13µs max=501.66ms p(50)=46.07µs p(95)=75.92µs p(99)=120.24µs + http_req_tls_handshaking.............: min=0s avg=29.45µs max=657.29ms p(50)=0s p(95)=0s p(99)=0s + http_req_waiting.....................: min=3.84ms avg=321.92ms max=3.4s p(50)=206.45ms p(95)=901.97ms p(99)=1.01s + http_reqs............................: 3291974 1807.469453/s + iteration_duration...................: min=18.62ms avg=328.21ms max=3.41s p(50)=215.77ms p(95)=907.48ms p(99)=1.01s + iterations...........................: 3289564 1806.146234/s + login_ui_enter_login_name_duration...: min=131.65ms avg=131.65ms max=131.65ms p(50)=131.65ms p(95)=131.65ms p(99)=131.65ms + login_ui_enter_password_duration.....: min=18.55ms avg=18.55ms max=18.55ms p(50)=18.55ms p(95)=18.55ms p(99)=18.55ms + login_ui_init_login_duration.........: min=68.72ms avg=68.72ms max=68.72ms p(50)=68.72ms p(95)=68.72ms p(99)=68.72ms + login_ui_token_duration..............: min=77.56ms avg=77.56ms max=77.56ms p(50)=77.56ms p(95)=77.56ms p(99)=77.56ms + oidc_token_duration..................: min=16.42ms avg=323.79ms max=3.4s p(50)=209.87ms p(95)=903.91ms p(99)=1.01s + org_create_org_duration..............: min=38.42ms avg=38.42ms max=38.42ms p(50)=38.42ms p(95)=38.42ms p(99)=38.42ms + user_add_machine_key_duration........: min=23.47ms avg=280.32ms max=851.94ms p(50)=304.18ms p(95)=437.53ms p(99)=443.37ms + user_create_machine_duration.........: min=63.91ms avg=406.02ms max=663.96ms p(50)=437.61ms p(95)=518.85ms p(99)=521.88ms + vus..................................: 425 min=0 max=600 + vus_max..............................: 600 min=600 max=600 + + +running (30m21.3s), 000/600 VUs, 3289564 complete and 0 interrupted iterations +default ✓ [======================================] 600 VUs 30m0s +``` + diff --git a/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/output.json b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/output.json new file mode 100644 index 0000000000..f03567d44a --- /dev/null +++ b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/output.json @@ -0,0 +1,1572 @@ +[ + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:12+00","p50":226.80441216,"p95":292.2581226923254,"p99":325.5444011804323}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:13+00","p50":303.4329753749999,"p95":530.6733049414297,"p99":567.30927307371}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:14+00","p50":302.74891633396584,"p95":604.7013469500243,"p99":614.6893857158778}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:15+00","p50":325.100155,"p95":606.6103585921124,"p99":609.8550800478059}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:16+00","p50":361.64220778124997,"p95":682.5986029211113,"p99":3295.896356099604}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:17+00","p50":311.9510276616327,"p95":795.2728765153314,"p99":808.5238720090455}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:18+00","p50":330.943432175,"p95":904.2621740605202,"p99":917.8023852704073}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:19+00","p50":394.07251646875005,"p95":876.7589186742251,"p99":883.9929505850307}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:20+00","p50":431.46734833710815,"p95":786.1183312083676,"p99":843.129903600502}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:21+00","p50":551.3915481388889,"p95":992.6629004539294,"p99":1017.6792653328623}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:22+00","p50":497.87218064347826,"p95":989.0976041999207,"p99":1001.9454134977383}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:23+00","p50":442.64701572592594,"p95":814.4198630706968,"p99":841.4281979419711}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:24+00","p50":411.5698667705882,"p95":749.5986867027285,"p99":760.7061443220067}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:25+00","p50":412.1297083766916,"p95":800.8400148923031,"p99":811.2535669033024}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:26+00","p50":507.8750002152381,"p95":850.039875055058,"p99":860.0256743397883}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:27+00","p50":465.25835430555554,"p95":792.79890057003,"p99":798.9528485138085}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:28+00","p50":500.7428103402778,"p95":799.9773754569674,"p99":805.3877535663701}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:29+00","p50":416.9319790435898,"p95":756.4934249753344,"p99":782.3338328457231}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:30+00","p50":409.446729,"p95":704.4358527261671,"p99":707.8622070821856}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:31+00","p50":431.10160412975705,"p95":742.780703663499,"p99":747.7094352509105}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:32+00","p50":438.8698693074792,"p95":723.874841749524,"p99":731.2911996430813}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:33+00","p50":460.77788237119114,"p95":736.802596085751,"p99":743.5967629006125}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:34+00","p50":457.8682860137363,"p95":781.8816769842254,"p99":787.9976388695852}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:35+00","p50":426.41345278947375,"p95":727.9498678832771,"p99":742.652598833263}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:36+00","p50":448.77417509418274,"p95":735.4066735172622,"p99":743.7479522655925}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:37+00","p50":428.2517828503759,"p95":728.8862330633025,"p99":733.758870425374}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:38+00","p50":492.0898813209876,"p95":785.8078790933473,"p99":795.2371716580127}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:39+00","p50":458.9457820831025,"p95":751.6501070733834,"p99":757.6296614049812}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:40+00","p50":412.6279474444445,"p95":720.0410543170241,"p99":725.3562274485259}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:41+00","p50":407.57438946541356,"p95":742.5514249260323,"p99":748.2140684291002}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:42+00","p50":415.9983718947368,"p95":754.795421153356,"p99":766.0024560400469}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:43+00","p50":428.1456869722222,"p95":759.0551689784448,"p99":768.3684850425636}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:44+00","p50":406.7604713939271,"p95":709.5277752653984,"p99":717.318463612515}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:45+00","p50":398.53694925,"p95":699.5980130596854,"p99":706.324271665933}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:46+00","p50":406.98302810526314,"p95":732.5767573436967,"p99":738.3985069516829}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:47+00","p50":433.37312517378916,"p95":745.2997198794175,"p99":753.5465345212392}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:48+00","p50":466.7073398229166,"p95":759.3042805307299,"p99":774.0043747179759}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:49+00","p50":401.19711447487185,"p95":708.7456584052499,"p99":721.3277307832102}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:50+00","p50":396.5683705333333,"p95":667.4736412516669,"p99":675.7801769646821}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:51+00","p50":435.5378857076024,"p95":709.7029266004154,"p99":731.2463297780371}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:52+00","p50":437.8546534166666,"p95":762.1523561613566,"p99":773.2566024162713}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:53+00","p50":402.36380296675895,"p95":735.6936697803411,"p99":767.8827300526118}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:54+00","p50":451.13125700864197,"p95":773.9584202721741,"p99":780.4959714461845}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:55+00","p50":451.77078472916673,"p95":762.2362345091318,"p99":769.1737047272173}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:56+00","p50":452.0772762576177,"p95":768.6665505212662,"p99":773.4509180908385}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:57+00","p50":443.1847585941358,"p95":858.6454454338701,"p99":874.375683597584}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:58+00","p50":447.6983186979167,"p95":847.6724164430321,"p99":857.2664667741503}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:48:59+00","p50":418.4935931745153,"p95":730.5123063135688,"p99":738.1423389611704}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:00+00","p50":410.84399471875,"p95":764.7363219285716,"p99":773.4497338869613}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:01+00","p50":415.41657381904764,"p95":737.5325494811408,"p99":748.9399632428275}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:02+00","p50":466.01099731944447,"p95":761.4918326233187,"p99":766.9979002530667}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:03+00","p50":420.8552623804714,"p95":755.2787398956343,"p99":762.4985182154425}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:04+00","p50":405.9713657708333,"p95":759.570963791907,"p99":772.0976435848588}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:05+00","p50":402.7055746527778,"p95":706.1553378323953,"p99":722.2648563290651}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:06+00","p50":603.9557779183502,"p95":882.9703191068454,"p99":896.0559822955025}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:07+00","p50":402.5235207282051,"p95":862.3186145463072,"p99":889.93196379501}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:08+00","p50":393.4791417612456,"p95":731.5256073816882,"p99":743.1763081727805}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:09+00","p50":421.79881482986116,"p95":742.14615406054,"p99":751.04443863171}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:10+00","p50":420.6259693653846,"p95":705.4855544442712,"p99":716.4504865600358}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:11+00","p50":424.8325983780864,"p95":716.2789417504985,"p99":723.4834368266775}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:12+00","p50":435.4987450416667,"p95":773.5136575421407,"p99":780.2459295902951}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:13+00","p50":397.34567678316324,"p95":728.7907058256036,"p99":737.519459765432}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:14+00","p50":404.8808237225,"p95":692.777211887369,"p99":700.265988128355}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:15+00","p50":410.788577341191,"p95":701.0943421127412,"p99":714.4601140642878}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:16+00","p50":411.32723394999994,"p95":687.8336204221143,"p99":700.6725694407385}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:17+00","p50":408.99720575303644,"p95":679.4106909582944,"p99":689.8313078634046}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:18+00","p50":420.90671505937917,"p95":696.2748467342478,"p99":709.5156723615902}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:19+00","p50":396.52008641176474,"p95":689.14304392858,"p99":696.0544223822286}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:20+00","p50":409.11422203,"p95":658.048629596464,"p99":667.2933994905724}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:21+00","p50":420.5113891553846,"p95":687.1677377225075,"p99":694.380941531006}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:22+00","p50":406.59691041124995,"p95":706.1960777471577,"p99":711.303240210816}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:23+00","p50":412.3075669327731,"p95":684.0290388795277,"p99":693.241049372172}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:24+00","p50":415.04967436617403,"p95":714.3118028306739,"p99":723.9417155101575}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:25+00","p50":550.6359299404432,"p95":718.7707048242565,"p99":728.2596685739746}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:26+00","p50":435.52909985114707,"p95":732.6213797765392,"p99":740.0437298251054}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:27+00","p50":402.13344296992483,"p95":710.5131414091744,"p99":726.6235718155953}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:28+00","p50":474.6721017916666,"p95":699.925626567794,"p99":707.1889199872609}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:29+00","p50":384.2069827687075,"p95":668.5948432548201,"p99":676.0194770475656}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:30+00","p50":412.5647846275,"p95":663.7107297831785,"p99":668.5755362201933}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:31+00","p50":407.8855693393818,"p95":676.9562713320016,"p99":687.5940798977341}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:32+00","p50":413.5626399730769,"p95":684.7764753432077,"p99":694.1630295202605}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:33+00","p50":409.66369116558445,"p95":683.7996788790921,"p99":689.077903628318}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:34+00","p50":372.45029607200007,"p95":717.1024892719398,"p99":729.5915745728115}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:35+00","p50":283.854023012755,"p95":654.0173563248285,"p99":675.8447505172043}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:36+00","p50":228.199990481118,"p95":625.1471177713042,"p99":634.5417552311715}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:37+00","p50":343.7394301213992,"p95":643.3884457395465,"p99":655.0087388927066}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:38+00","p50":370.30616374485595,"p95":614.9821599007743,"p99":620.0343778239428}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:39+00","p50":347.4672139655172,"p95":619.4346667558254,"p99":624.4213321331309}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:40+00","p50":372.59909436747495,"p95":606.0382469108029,"p99":622.4495622318841}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:41+00","p50":442.96595134989644,"p95":743.1566572614927,"p99":748.2620603947355}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:42+00","p50":371.69758059839995,"p95":603.7830400610445,"p99":608.7138792060418}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:43+00","p50":360.5478799744897,"p95":623.5477211780434,"p99":632.7596498567217}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:44+00","p50":376.95264344770413,"p95":573.689466427619,"p99":578.4262014759428}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:45+00","p50":397.75282972595284,"p95":572.1549721155669,"p99":578.7018986697082}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:46+00","p50":364.7357128252551,"p95":613.7408748421282,"p99":620.6733771638262}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:47+00","p50":358.09714828927537,"p95":622.191458574817,"p99":630.3467183918447}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:48+00","p50":382.4554115328,"p95":664.856074918337,"p99":675.0328361906207}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:49+00","p50":375.9703518077274,"p95":643.2956012472607,"p99":654.0688099045517}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:50+00","p50":425.386544896,"p95":592.2911675545159,"p99":605.1470735457043}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:51+00","p50":489.9446529338374,"p95":711.8262238083043,"p99":719.2038440433616}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:52+00","p50":459.93697411708996,"p95":704.239300320098,"p99":709.1139587590538}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:53+00","p50":365.85698195200007,"p95":647.4872552974532,"p99":657.4827430688334}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:54+00","p50":388.5069847892208,"p95":630.7325238001844,"p99":637.3880403778438}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:55+00","p50":419.01836264852614,"p95":752.4206307011402,"p99":792.6405464470555}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:56+00","p50":430.1625064453125,"p95":682.2026101071048,"p99":694.9236202583082}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:57+00","p50":432.8853055670034,"p95":680.3210663578649,"p99":691.0093505166683}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:58+00","p50":416.58995791587904,"p95":645.0579354559917,"p99":684.3778562265795}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:49:59+00","p50":418.5376514627926,"p95":827.8902854139619,"p99":836.5218520212316}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:00+00","p50":421.3051377208164,"p95":685.2291942709033,"p99":690.4942341890377}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:01+00","p50":452.24706221815114,"p95":588.1298611939612,"p99":593.5040999219483}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:02+00","p50":492.29989137499996,"p95":561.5653214087486,"p99":571.8263091848127}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:03+00","p50":489.75885080219786,"p95":548.676679153853,"p99":556.5360223434325}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:04+00","p50":497.8745409031065,"p95":544.6851644578139,"p99":560.2248442819322}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:05+00","p50":455.005084376734,"p95":532.6513155047435,"p99":539.1782925826661}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:06+00","p50":471.8711070784314,"p95":523.6144292166545,"p99":527.9102150556016}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:07+00","p50":475.6684936034043,"p95":538.5408071817833,"p99":554.3136487866366}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:08+00","p50":467.84446866666667,"p95":575.3951181532308,"p99":584.2008117071358}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:09+00","p50":483.77599231049464,"p95":716.9601926080331,"p99":738.7715975835289}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:10+00","p50":407.53128594619795,"p95":784.5176354622466,"p99":795.3790297865568}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:11+00","p50":386.58089762637354,"p95":613.4513847183345,"p99":624.38416028813}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:12+00","p50":415.75270722427985,"p95":617.5989623086884,"p99":625.0056494347009}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:13+00","p50":456.88584258093283,"p95":532.8469605844557,"p99":550.5269924744819}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:14+00","p50":458.7571697816667,"p95":524.7027316138111,"p99":534.4257825778341}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:15+00","p50":408.414303157828,"p95":560.9345056709991,"p99":564.6428887877922}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:16+00","p50":355.4627526747287,"p95":625.2451536126129,"p99":639.0737019252313}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:17+00","p50":376.02686017497666,"p95":592.039029793558,"p99":597.5785386275004}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:18+00","p50":360.4339312513733,"p95":638.4051154633739,"p99":644.9174099114705}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:19+00","p50":402.4932366642442,"p95":587.1922239152481,"p99":596.8850157460435}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:20+00","p50":339.4875431888,"p95":584.7793447289755,"p99":595.4732819799212}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:21+00","p50":360.1301634992993,"p95":608.2131779405853,"p99":612.3811162113724}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:22+00","p50":362.6070534592,"p95":623.389586216422,"p99":640.5476464675612}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:23+00","p50":389.492971963843,"p95":801.6569322353624,"p99":821.2886842968358}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:24+00","p50":384.83197409890107,"p95":787.157922307184,"p99":799.6211665164639}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:25+00","p50":412.5013164788462,"p95":650.8541302249788,"p99":662.3041114946258}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:26+00","p50":432.72368526879995,"p95":690.163423154622,"p99":697.9938324145056}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:27+00","p50":403.35415059602195,"p95":636.7729564404534,"p99":641.8302764846309}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:28+00","p50":362.52661583539094,"p95":657.4501476911913,"p99":666.0768459543287}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:29+00","p50":369.1248249978437,"p95":609.4057586098606,"p99":618.1865465091861}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:30+00","p50":365.356377839599,"p95":755.0509182986417,"p99":761.3410626697373}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:31+00","p50":365.0124088251028,"p95":636.9401989987583,"p99":642.6335300001899}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:32+00","p50":342.8897912445888,"p95":622.8535101833877,"p99":627.1928946555191}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:33+00","p50":348.45082306377554,"p95":613.1249073529467,"p99":626.9968191756367}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:34+00","p50":372.3294066874237,"p95":598.407435238691,"p99":605.3392929242073}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:35+00","p50":375.22492713296407,"p95":538.9610458027179,"p99":557.432847575857}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:36+00","p50":433.311659312049,"p95":528.364489958729,"p99":532.193407268551}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:37+00","p50":449.1878734861111,"p95":704.5159645699721,"p99":715.8511747609227}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:38+00","p50":360.3587457794157,"p95":643.2928279478427,"p99":712.205293690036}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:39+00","p50":348.3788961377551,"p95":640.9216019297016,"p99":645.3072949592598}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:40+00","p50":353.49914694238686,"p95":621.6028064393785,"p99":636.8406522121663}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:41+00","p50":373.3167136357142,"p95":570.0019849209796,"p99":573.7581773026569}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:42+00","p50":357.56970956071433,"p95":601.0993670590935,"p99":611.8615478315987}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:43+00","p50":346.90371787662343,"p95":623.8681150172232,"p99":636.5749566754863}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:44+00","p50":349.3732869132883,"p95":596.8119444327375,"p99":600.9540392173474}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:45+00","p50":374.78892053846164,"p95":575.9818225168664,"p99":583.7270876409517}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:46+00","p50":345.8713620841466,"p95":596.5350100201861,"p99":603.3445251167043}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:47+00","p50":332.87316409136724,"p95":609.8015500667195,"p99":614.2479382538163}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:48+00","p50":356.7742658069498,"p95":622.6198995116966,"p99":627.6185368149004}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:49+00","p50":367.2529607624224,"p95":588.0421102113482,"p99":606.2699005604563}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:50+00","p50":383.26963749652776,"p95":747.2060991746678,"p99":771.0356866865832}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:51+00","p50":385.21009446880004,"p95":781.9330828095312,"p99":788.9278110613296}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:52+00","p50":416.4767072585522,"p95":611.7101305110978,"p99":617.7196325104279}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:53+00","p50":385.5045756047619,"p95":577.7149616069698,"p99":583.3101596800002}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:54+00","p50":381.0876974738501,"p95":614.5928472955281,"p99":624.2782485847113}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:55+00","p50":368.74362815384615,"p95":609.7233401324729,"p99":623.6787107992614}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:56+00","p50":422.29352957500004,"p95":708.2417862980911,"p99":735.3426397477824}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:57+00","p50":386.01071919748523,"p95":677.7615183618117,"p99":686.7920626195282}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:58+00","p50":407.3174838275862,"p95":554.6910483694928,"p99":579.1602405119359}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:50:59+00","p50":438.6231846927083,"p95":532.5737698967428,"p99":539.9698709620852}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:00+00","p50":428.97383684166664,"p95":547.8944812838633,"p99":551.3247466169955}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:01+00","p50":444.2833721041666,"p95":731.0742241239658,"p99":738.3359239254429}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:02+00","p50":370.3705239948347,"p95":591.2026930531902,"p99":730.9656601837193}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:03+00","p50":352.04607326112904,"p95":603.8700184627457,"p99":610.2750540140328}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:04+00","p50":339.49542862121217,"p95":771.6459411170196,"p99":791.3737546773045}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:05+00","p50":371.82738073214284,"p95":769.8806482653571,"p99":778.1739162617818}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:06+00","p50":356.5057894672269,"p95":611.9300986130737,"p99":618.9226640695754}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:07+00","p50":341.18530891676573,"p95":613.37846476256,"p99":630.6470278822236}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:08+00","p50":377.62977518783066,"p95":600.9564006724934,"p99":606.0066757214088}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:09+00","p50":400.06201106818185,"p95":588.9921278127359,"p99":597.6219048073037}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:10+00","p50":469.6124728501276,"p95":507.8713022845163,"p99":512.2391160591155}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:11+00","p50":465.3915782011966,"p95":508.5869999165645,"p99":513.0594066214951}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:12+00","p50":462.35022011764704,"p95":538.7994547087422,"p99":545.4097272502327}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:13+00","p50":453.65880751785716,"p95":516.1190879170532,"p99":520.8977409879834}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:14+00","p50":417.9012554795918,"p95":557.4726026769633,"p99":561.0861971045294}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:15+00","p50":404.88177406700856,"p95":586.764306585883,"p99":604.438477984477}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:16+00","p50":388.2280703899999,"p95":596.2949647427317,"p99":602.7544623026857}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:17+00","p50":367.10503423299235,"p95":622.4129193282848,"p99":630.9093686193967}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:18+00","p50":473.7472404122807,"p95":883.5598795878536,"p99":888.337911482343}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:19+00","p50":412.3995760147657,"p95":707.0983933417726,"p99":722.1561369373877}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:20+00","p50":402.4538933333333,"p95":658.4392603642766,"p99":664.7490158447865}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:21+00","p50":598.2525035470086,"p95":719.0397146329078,"p99":723.4454094811298}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:22+00","p50":525.8519025381945,"p95":695.5202750158933,"p99":708.4994033057125}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:23+00","p50":462.3782369373085,"p95":625.4916643704968,"p99":632.2910380481025}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:24+00","p50":379.417122388986,"p95":678.4516896246773,"p99":697.153082430985}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:25+00","p50":400.450619127601,"p95":671.869069295031,"p99":676.716201042439}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:26+00","p50":455.5205367051091,"p95":731.9166373745966,"p99":743.4956424242522}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:27+00","p50":416.37770459979754,"p95":708.9723608958608,"p99":717.6966751609726}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:28+00","p50":410.63015204212854,"p95":708.831857154851,"p99":717.1280823339698}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:29+00","p50":440.64005200021853,"p95":625.5249167863112,"p99":657.84483175196}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:30+00","p50":445.01255220221606,"p95":564.4336190548449,"p99":570.4892565588441}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:31+00","p50":398.1348405102975,"p95":672.937984109596,"p99":686.0778438709597}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:32+00","p50":425.3150955138462,"p95":780.3791668114394,"p99":785.2051680906916}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:33+00","p50":394.23512121088424,"p95":811.3280014119114,"p99":816.0192819123373}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:34+00","p50":417.21185435999996,"p95":833.6738196784024,"p99":843.961383004479}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:35+00","p50":478.0564773195266,"p95":577.5856883993237,"p99":587.9627369951706}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:36+00","p50":465.5578047019323,"p95":587.418898572271,"p99":593.8787683069947}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:37+00","p50":421.8139999101852,"p95":595.0740281829999,"p99":599.7325618613144}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:38+00","p50":384.5595757693548,"p95":684.1259072964981,"p99":696.8635033723716}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:39+00","p50":406.800125125,"p95":693.3090683629177,"p99":708.1508995154521}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:40+00","p50":369.6137988088975,"p95":687.7424862741206,"p99":703.2106915179589}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:41+00","p50":374.30102730328815,"p95":631.9047757410954,"p99":644.2292020367616}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:42+00","p50":372.2715728284023,"p95":655.4377074018795,"p99":661.85242508289}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:43+00","p50":358.9827671530612,"p95":639.4918758388802,"p99":652.8116377957891}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:44+00","p50":395.4490450133136,"p95":633.4140875866071,"p99":645.4730888888424}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:45+00","p50":443.510389592711,"p95":663.4555903849742,"p99":669.0866838478254}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:46+00","p50":374.11618639273235,"p95":636.4176351861709,"p99":653.166355654519}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:47+00","p50":403.08515472319993,"p95":611.009638203096,"p99":622.3788845307844}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:48+00","p50":395.5242461388889,"p95":863.4966024185178,"p99":870.9960908577087}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:49+00","p50":382.04343696800004,"p95":678.8660665771786,"p99":687.0687892165099}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:50+00","p50":378.3448406249999,"p95":664.7654633614364,"p99":669.8300200358219}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:51+00","p50":392.9853373117761,"p95":685.1706183667212,"p99":694.748059358522}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:52+00","p50":422.38154359199996,"p95":686.1398069218685,"p99":704.5876044317861}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:53+00","p50":417.3495001096667,"p95":634.4525951371139,"p99":643.8174067770236}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:54+00","p50":482.88150330606067,"p95":678.7300956813724,"p99":684.5747324147272}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:55+00","p50":465.68952853825914,"p95":614.91557734119,"p99":628.8289935754028}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:56+00","p50":520.8761767394958,"p95":731.3535124604737,"p99":744.4433378233707}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:57+00","p50":445.9754837881944,"p95":683.9369895756907,"p99":689.6404164612092}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:58+00","p50":446.54599991666663,"p95":666.4240210925274,"p99":675.1943436964067}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:51:59+00","p50":452.08607134375,"p95":570.7820723958048,"p99":574.3075128966129}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:00+00","p50":418.13952315135134,"p95":581.8387637917692,"p99":589.0830006667024}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:01+00","p50":405.02116921119995,"p95":605.472942482645,"p99":612.2958597429852}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:02+00","p50":409.54392618137257,"p95":679.9712996029536,"p99":686.2727312252081}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:03+00","p50":493.22685321481475,"p95":934.1493669128434,"p99":951.3018140389062}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:04+00","p50":374.9218868502564,"p95":636.9359168552304,"p99":714.8766348354679}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:05+00","p50":357.73509516542833,"p95":594.1236690621718,"p99":601.8233755845675}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:06+00","p50":385.1197415457071,"p95":575.3490096624965,"p99":581.8756687547401}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:07+00","p50":357.2927178761688,"p95":567.5443225154715,"p99":594.8088705652061}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:08+00","p50":342.6088031269841,"p95":693.3756196960678,"p99":756.171762371769}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:09+00","p50":164.274631500821,"p95":474.7572392383728,"p99":505.73527372643144}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:10+00","p50":255.5011276847196,"p95":588.1954560547108,"p99":637.1430391830091}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:11+00","p50":301.1834004927083,"p95":761.8532123774597,"p99":834.3536907588781}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:12+00","p50":251.23248290045248,"p95":635.4677202939808,"p99":654.3610577415475}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:13+00","p50":244.07354622235434,"p95":662.1529033688068,"p99":678.9184336156438}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:14+00","p50":300.03227843999997,"p95":777.3799199541058,"p99":831.4321709016666}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:15+00","p50":126.34014755176534,"p95":464.4302259185243,"p99":663.7810136536917}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:16+00","p50":155.4125526722153,"p95":440.7519443465454,"p99":472.49471414904906}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:17+00","p50":396.857280410124,"p95":637.2544978755471,"p99":678.2911184449491}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:18+00","p50":195.00826330873312,"p95":552.4894607225327,"p99":635.5806166694384}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:19+00","p50":263.9401334471879,"p95":351.2534324163608,"p99":368.8502524036613}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:20+00","p50":286.6611157494824,"p95":428.77245018209567,"p99":492.6384062530812}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:21+00","p50":176.67858865156796,"p95":447.70365700823646,"p99":510.7445058338642}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:22+00","p50":163.08070574898989,"p95":266.02626973784004,"p99":306.99897413412623}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:23+00","p50":214.14509651277137,"p95":462.3455325004555,"p99":505.51126746284626}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:24+00","p50":257.8253682568,"p95":612.4873631391632,"p99":619.9281115673249}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:25+00","p50":362.7117153703703,"p95":627.6623020161541,"p99":633.7193049835988}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:26+00","p50":404.4138936936,"p95":660.2189380065778,"p99":676.5585505878367}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:27+00","p50":361.5943798171653,"p95":660.9586459241847,"p99":667.9959168182934}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:28+00","p50":363.35087883298263,"p95":626.7875418584379,"p99":632.035047176714}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:29+00","p50":365.38408870020226,"p95":618.9002705755073,"p99":629.6239317327271}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:30+00","p50":380.10578646924813,"p95":726.6445653426207,"p99":742.2880040484048}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:31+00","p50":377.2878279600001,"p95":768.9167348360896,"p99":786.6005625172917}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:32+00","p50":347.4075065749761,"p95":625.9266753254818,"p99":636.7086272154684}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:33+00","p50":359.96400332080003,"p95":623.8085246868693,"p99":628.8405018270073}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:34+00","p50":396.9040130161702,"p95":680.4320052316542,"p99":687.2059590439226}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:35+00","p50":375.3446913357227,"p95":602.0119405320027,"p99":617.613106475734}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:36+00","p50":370.7746549230769,"p95":610.5042268945297,"p99":617.1611862451742}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:37+00","p50":342.71319194382977,"p95":621.4797224856018,"p99":626.0313314140433}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:38+00","p50":355.74100981242236,"p95":647.4644474769898,"p99":668.4408809828594}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:39+00","p50":375.17325135972817,"p95":603.8964705429031,"p99":610.8943889449291}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:40+00","p50":382.0582485484163,"p95":631.4683007437576,"p99":641.9588050748137}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:41+00","p50":369.0162769695999,"p95":630.6992709493747,"p99":640.2285279046243}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:42+00","p50":351.4925819392,"p95":617.0336495186227,"p99":638.2860098500935}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:43+00","p50":285.6001157316738,"p95":610.4324939255001,"p99":615.2426353728499}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:44+00","p50":322.30777599338376,"p95":785.2269342387959,"p99":793.4643356911257}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:45+00","p50":349.8626594226659,"p95":630.5552253217204,"p99":637.3865722996599}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:46+00","p50":338.3902687866515,"p95":613.3403546781292,"p99":622.2850355860581}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:47+00","p50":329.20043878666667,"p95":630.7322607952085,"p99":636.014993685452}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:48+00","p50":307.3182582095442,"p95":661.0490589558112,"p99":667.0619627316141}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:49+00","p50":355.53915351265925,"p95":636.8142857547188,"p99":647.5494135579846}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:50+00","p50":356.38252284479995,"p95":634.7825503678318,"p99":643.0242592849719}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:51+00","p50":356.6316700256411,"p95":663.9519691298738,"p99":670.4871114375591}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:52+00","p50":349.2092897708333,"p95":663.2086453174261,"p99":668.8362132572612}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:53+00","p50":338.6763881048889,"p95":668.9006435058178,"p99":677.966159503477}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:54+00","p50":351.87312590689027,"p95":653.0851346078426,"p99":657.7467503982956}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:55+00","p50":332.9488453248,"p95":645.5752964984646,"p99":658.1544685905751}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:56+00","p50":369.78285532341465,"p95":692.5060559667758,"p99":701.1046231800774}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:57+00","p50":356.79106254166663,"p95":677.1752809707662,"p99":680.5580656843603}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:58+00","p50":320.432187298,"p95":781.1398428529683,"p99":786.3505499954185}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:52:59+00","p50":329.156786433432,"p95":626.5256524481316,"p99":640.7170072422556}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:00+00","p50":328.8153335326388,"p95":626.409337027074,"p99":635.6224405160048}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:01+00","p50":332.85542062193366,"p95":605.4878439803398,"p99":621.1739930662453}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:02+00","p50":309.6742276313912,"p95":638.1285552234777,"p99":650.2090760276666}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:03+00","p50":313.3502051594203,"p95":607.7836876136743,"p99":617.244895795074}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:04+00","p50":325.77003914005377,"p95":662.3678224945129,"p99":667.8322137970546}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:05+00","p50":318.3702340357143,"p95":663.5431215109098,"p99":678.4491160045898}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:06+00","p50":315.25020473543395,"p95":638.4336660928927,"p99":655.2059054488805}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:07+00","p50":301.0933725952382,"p95":634.6598086210747,"p99":647.0606034084377}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:08+00","p50":318.3032956826923,"p95":683.3647817434493,"p99":687.9348285562647}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:09+00","p50":331.66922423529405,"p95":663.8678781628323,"p99":669.016371442879}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:10+00","p50":321.26857333125,"p95":643.691318744505,"p99":649.358272380354}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:11+00","p50":348.6460315931911,"p95":819.9454270889114,"p99":837.6328219895953}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:12+00","p50":310.7112166172619,"p95":815.2995748503038,"p99":843.001807831867}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:13+00","p50":298.3131429881448,"p95":636.0441370744395,"p99":643.8477532160954}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:14+00","p50":310.03954834951156,"p95":637.2070634947132,"p99":656.7840341436945}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:15+00","p50":294.91974056853337,"p95":606.091997160328,"p99":612.395805462838}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:16+00","p50":303.25969799682036,"p95":615.8764175661828,"p99":621.6841779629956}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:17+00","p50":295.5506302640306,"p95":649.7997413311195,"p99":663.1509075583609}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:18+00","p50":296.90483919999997,"p95":652.9598375887939,"p99":660.4511199634256}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:19+00","p50":299.23584535297624,"p95":632.6199377873718,"p99":636.5717473390979}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:20+00","p50":299.4118923051021,"p95":637.2818326454079,"p99":650.814083588255}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:21+00","p50":312.7747940887574,"p95":686.4318958643673,"p99":702.6907062311808}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:22+00","p50":264.12437636263735,"p95":700.9194072800915,"p99":709.2881517662963}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:23+00","p50":290.821740178726,"p95":666.0563433013799,"p99":674.0873617480472}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:24+00","p50":303.2561826384,"p95":690.8693138778905,"p99":696.2771982186091}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:25+00","p50":351.81318538642654,"p95":936.406513003771,"p99":949.3505965263339}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:26+00","p50":319.33129211465507,"p95":731.5330931474365,"p99":961.226134281539}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:27+00","p50":303.6451651555556,"p95":690.5479372066707,"p99":696.8915747376066}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:28+00","p50":323.51722734027777,"p95":681.3859653282356,"p99":685.3632883283954}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:29+00","p50":298.15773897325096,"p95":647.1526161497069,"p99":656.1373772377183}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:30+00","p50":305.572436173913,"p95":655.8304550115962,"p99":668.4998585991116}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:31+00","p50":297.06561033509155,"p95":642.9820389538041,"p99":647.8591559138214}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:32+00","p50":292.6476819622078,"p95":643.3949921836866,"p99":651.0039663210882}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:33+00","p50":292.88619784879035,"p95":612.1451386576246,"p99":627.832207408565}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:34+00","p50":304.4478905206349,"p95":640.3877508255373,"p99":645.1248536675301}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:35+00","p50":300.6705247854545,"p95":679.9338071744055,"p99":688.3793757144471}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:36+00","p50":282.42214934693874,"p95":647.2805326941217,"p99":685.3124483070312}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:37+00","p50":263.85116531520003,"p95":615.0436495332355,"p99":623.8560762490455}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:38+00","p50":296.46612922559177,"p95":696.1862251666557,"p99":702.0301262851709}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:39+00","p50":319.58374738888887,"p95":843.3647878001722,"p99":861.9868619975499}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:40+00","p50":302.62902569078955,"p95":818.8502470660511,"p99":830.2112044227189}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:41+00","p50":309.8049117476923,"p95":687.334147928736,"p99":699.113329749843}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:42+00","p50":313.0739105504202,"p95":713.0917765917924,"p99":717.8062390625516}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:43+00","p50":300.4779708380342,"p95":689.3546673757392,"p99":693.2867748922246}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:44+00","p50":281.16354708824406,"p95":656.2379988605295,"p99":665.8852016420578}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:45+00","p50":280.5616689768889,"p95":639.9132649943256,"p99":644.6847547653447}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:46+00","p50":266.361508154321,"p95":672.8594011896118,"p99":689.9105531431943}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:47+00","p50":279.92702474720005,"p95":658.1996494244787,"p99":675.236689083622}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:48+00","p50":291.42704209171603,"p95":694.0865137496633,"p99":702.3238066020883}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:49+00","p50":287.39434861224487,"p95":682.1183832908195,"p99":691.5131418065803}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:50+00","p50":291.0341432905983,"p95":699.0299344247107,"p99":722.9502988323719}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:51+00","p50":290.3077052477811,"p95":665.9200186653989,"p99":691.2419730130747}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:52+00","p50":289.64802978630195,"p95":671.5918464115982,"p99":677.0420445862854}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:53+00","p50":275.56653485,"p95":668.2699517022523,"p99":684.7573509427801}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:54+00","p50":297.1023152942386,"p95":715.5816342824744,"p99":721.3246603464507}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:55+00","p50":289.62048778498604,"p95":647.7767691140784,"p99":661.2631727436885}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:56+00","p50":294.0458508848001,"p95":722.7601232487775,"p99":738.9325955041343}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:57+00","p50":284.17493866442584,"p95":693.6386176826015,"p99":706.0548224236105}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:58+00","p50":282.61690152666677,"p95":698.9903217245018,"p99":703.3532038255767}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:53:59+00","p50":285.2980837516991,"p95":662.4802889536685,"p99":667.3059553139794}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:00+00","p50":283.571914424,"p95":670.42912501643,"p99":676.9185654580272}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:01+00","p50":292.9302036988304,"p95":673.8804127000661,"p99":681.8145599218227}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:02+00","p50":282.2703726693742,"p95":690.8990538964705,"p99":703.7295584679285}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:03+00","p50":280.0802174979424,"p95":685.1101699794355,"p99":694.0205898866071}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:04+00","p50":277.46401249599995,"p95":655.5665967746236,"p99":662.1209854829482}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:05+00","p50":272.9045880272,"p95":663.9712726511856,"p99":675.7449514218553}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:06+00","p50":287.6282553353909,"p95":679.388089252062,"p99":695.6612493125804}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:07+00","p50":262.6406955803571,"p95":638.8424688755766,"p99":649.3238526811122}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:08+00","p50":295.8836518543956,"p95":693.7499185591283,"p99":702.0300513304205}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:09+00","p50":274.0410787394694,"p95":667.9172125909963,"p99":678.1473233969523}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:10+00","p50":269.77943247160493,"p95":672.0414363366071,"p99":677.4842293049255}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:11+00","p50":272.23194673533163,"p95":664.0070718566487,"p99":672.1771786204057}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:12+00","p50":259.4638945079051,"p95":665.4851705760117,"p99":670.2403764485686}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:13+00","p50":243.76441329033187,"p95":658.0080070990135,"p99":664.3569611031443}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:14+00","p50":264.41482071933655,"p95":640.9894626976096,"p99":647.6222632399524}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:15+00","p50":242.31905302234847,"p95":624.7477457150846,"p99":636.2856108117535}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:16+00","p50":262.8225203980548,"p95":622.7520511943692,"p99":629.6073372544602}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:17+00","p50":270.7257813264746,"p95":671.5792752259343,"p99":680.0005540703684}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:18+00","p50":273.0466552163743,"p95":702.2551194857155,"p99":710.5093586101434}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:19+00","p50":262.64552123182443,"p95":665.5621630095869,"p99":672.5648912502667}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:20+00","p50":268.09675005710164,"p95":642.5712108394072,"p99":655.0510840565609}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:21+00","p50":318.6219786190476,"p95":793.8495922908361,"p99":813.6266728253286}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:22+00","p50":351.07352785941043,"p95":846.0802414828537,"p99":858.4029383038148}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:23+00","p50":287.2998321712,"p95":768.6478425095518,"p99":803.8408422806278}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:24+00","p50":275.2570886153191,"p95":705.0033462655473,"p99":715.5233667560323}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:25+00","p50":279.5222225295858,"p95":678.1155322757082,"p99":700.557279490558}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:26+00","p50":307.8247607977315,"p95":771.3450923635511,"p99":781.8084637635963}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:27+00","p50":274.03487122989515,"p95":721.5225230402683,"p99":732.0983889731456}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:28+00","p50":292.2846523744,"p95":745.3726260759754,"p99":750.8578240255183}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:29+00","p50":272.225401954142,"p95":683.5298947183196,"p99":688.8700566241619}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:30+00","p50":270.92377943909463,"p95":640.0135102387355,"p99":653.9840734500049}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:31+00","p50":250.2492111789941,"p95":633.6156162294865,"p99":639.8015166451185}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:32+00","p50":246.28922130500004,"p95":625.1218177236534,"p99":635.1986205668351}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:33+00","p50":238.7422096598865,"p95":639.8887803416238,"p99":654.0826766549405}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:34+00","p50":246.27259926738878,"p95":613.7584682597028,"p99":617.89009266799}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:35+00","p50":247.95113328249155,"p95":636.0299732694082,"p99":653.6369440095942}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:36+00","p50":242.32258594463767,"p95":655.349893067326,"p99":682.7039280331256}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:37+00","p50":239.9111089553333,"p95":681.0343292542955,"p99":689.9018559273668}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:38+00","p50":243.8699973807398,"p95":633.8326593166854,"p99":639.2036816555584}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:39+00","p50":238.82185397222221,"p95":639.2467795113897,"p99":649.595518381766}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:40+00","p50":241.73935877504107,"p95":624.3212632302875,"p99":631.7395465323078}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:41+00","p50":238.35479853210464,"p95":641.9266069387892,"p99":664.3314986847726}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:42+00","p50":234.78879448632577,"p95":671.112694058261,"p99":688.3151707763683}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:43+00","p50":231.4698275974663,"p95":654.1892065084556,"p99":660.8917181047884}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:44+00","p50":242.54090641545218,"p95":642.1412865293581,"p99":649.6031418724416}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:45+00","p50":241.28275902923076,"p95":682.8164110527035,"p99":692.3135558791889}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:46+00","p50":237.0770091914329,"p95":666.5249303754576,"p99":689.0999907000663}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:47+00","p50":240.64463510499996,"p95":662.9252172387578,"p99":668.4643593951571}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:48+00","p50":238.0080571478167,"p95":686.2493926558727,"p99":707.9315894053622}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:49+00","p50":247.1695555204082,"p95":749.2801544876793,"p99":755.3653963422298}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:50+00","p50":243.5687278525036,"p95":676.965571696419,"p99":687.6871071086476}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:51+00","p50":249.14525932005498,"p95":721.1249923870673,"p99":736.0123024452805}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:52+00","p50":238.9201514833333,"p95":703.162220618004,"p99":709.0099134603381}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:53+00","p50":242.849972628,"p95":721.764883288584,"p99":747.7929210711366}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:54+00","p50":232.7721265178571,"p95":772.5907327866541,"p99":796.9833656082537}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:55+00","p50":256.4294420121795,"p95":698.487584675351,"p99":712.396876590846}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:56+00","p50":274.089313602071,"p95":826.6122733176485,"p99":835.898947620243}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:57+00","p50":292.36599254640004,"p95":817.6060353836177,"p99":831.5225533782772}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:58+00","p50":258.4740387639715,"p95":771.9142958157206,"p99":782.1315777711926}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:54:59+00","p50":242.12705497711366,"p95":691.4840919884089,"p99":704.3464223648309}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:00+00","p50":232.97405207333335,"p95":701.8029744946218,"p99":721.5540898158315}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:01+00","p50":244.82735258986017,"p95":695.2966150903005,"p99":702.0943572690699}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:02+00","p50":248.4652205379932,"p95":726.7178498503798,"p99":742.1682170352701}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:03+00","p50":229.32590207166666,"p95":709.9264097962136,"p99":732.527628929032}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:04+00","p50":243.47572288333333,"p95":711.7839882213822,"p99":722.4893102523118}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:05+00","p50":247.08061551055124,"p95":702.7643541701316,"p99":712.3316519410257}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:06+00","p50":237.03117753333333,"p95":692.0418170862577,"p99":705.0981327885218}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:07+00","p50":249.9135775679293,"p95":772.2929395932864,"p99":781.731680622897}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:08+00","p50":243.5706487121053,"p95":768.5529959067401,"p99":782.4497198986277}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:09+00","p50":244.37127118656056,"p95":719.4867558109702,"p99":729.2180710528072}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:10+00","p50":251.9824111158217,"p95":738.6562266913782,"p99":754.9880389892362}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:11+00","p50":262.14502351785717,"p95":743.8834328082988,"p99":757.2525385716966}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:12+00","p50":264.452808815126,"p95":765.3046968813628,"p99":775.9489665570092}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:13+00","p50":255.74714583035717,"p95":763.0658507349609,"p99":768.649354729896}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:14+00","p50":243.1528526190476,"p95":761.1304856794345,"p99":775.0081769723434}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:15+00","p50":261.94032211209054,"p95":800.5162115905329,"p99":809.1471898139939}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:16+00","p50":250.99279472093025,"p95":761.6495503668931,"p99":790.674611022946}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:17+00","p50":224.9328364084423,"p95":756.0531455814463,"p99":777.9395171602923}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:18+00","p50":252.45311942361113,"p95":764.9938522369454,"p99":774.5727910088247}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:19+00","p50":247.09727205555555,"p95":751.4514608371553,"p99":758.4908630765798}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:20+00","p50":235.7388346140056,"p95":721.4527893256536,"p99":733.1931677769236}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:21+00","p50":250.16639446551724,"p95":730.5955149651177,"p99":737.7045854336993}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:22+00","p50":252.84084534324325,"p95":734.4346385985481,"p99":742.6080628574495}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:23+00","p50":240.35358650522645,"p95":711.9069209488265,"p99":723.2774345165004}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:24+00","p50":250.3239189463709,"p95":737.4804686358243,"p99":751.7088402050593}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:25+00","p50":244.5364812688,"p95":749.7515964322056,"p99":764.9318111497946}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:26+00","p50":259.81013830019987,"p95":831.4412302978195,"p99":845.1024340798117}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:27+00","p50":255.15114776000001,"p95":812.9676785565771,"p99":821.3279261162941}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:28+00","p50":275.3796407390476,"p95":878.8695568276495,"p99":885.3851973887414}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:29+00","p50":279.4027654046639,"p95":825.637755314743,"p99":844.8376435073235}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:30+00","p50":245.74855809770116,"p95":712.0954913423433,"p99":724.7946684734795}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:31+00","p50":253.46724091759998,"p95":734.0042748735713,"p99":738.8637176973417}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:32+00","p50":255.92581115204425,"p95":728.3988044658856,"p99":737.6780319995992}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:33+00","p50":250.42883529591836,"p95":724.7984553125199,"p99":732.370983261198}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:34+00","p50":260.4380270095999,"p95":718.6143731128219,"p99":724.6715958288457}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:35+00","p50":201.33019831914893,"p95":752.5778907369753,"p99":769.7572387384106}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:36+00","p50":169.20643144570707,"p95":730.5435508545249,"p99":782.7229349845966}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:37+00","p50":183.05674777777782,"p95":724.1677655350303,"p99":742.4839430966168}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:38+00","p50":158.56625155555557,"p95":766.3065459049078,"p99":777.3544557038597}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:39+00","p50":485.0751572298137,"p95":677.0424665374495,"p99":759.1486301941786}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:40+00","p50":201.3733420920635,"p95":836.9592618539845,"p99":846.7103280634542}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:41+00","p50":232.39705721521992,"p95":862.2756585942573,"p99":877.758485503507}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:42+00","p50":128.2625035855227,"p95":895.2264001792801,"p99":900.292680323524}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:43+00","p50":226.5870678980843,"p95":898.5049960998175,"p99":906.6912407893841}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:44+00","p50":262.7600405711111,"p95":832.2745774361233,"p99":849.2510660361518}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:45+00","p50":84.549608028,"p95":777.9105285699915,"p99":798.0791236794763}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:46+00","p50":350.6869881555555,"p95":698.6492300859114,"p99":709.1626367998631}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:47+00","p50":116.99418901984129,"p95":615.4465244944244,"p99":623.0617059646535}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:48+00","p50":499.84596038165677,"p95":707.1909228488768,"p99":713.6849456412924}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:49+00","p50":318.1893151428571,"p95":826.7673550655554,"p99":847.2425123798881}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:50+00","p50":204.4552942547619,"p95":878.5898380204894,"p99":904.5446883332165}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:51+00","p50":292.54369039583327,"p95":1063.051127271449,"p99":1089.80124037696}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:52+00","p50":215.86772523918367,"p95":1139.9798625612364,"p99":1151.461168179974}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:53+00","p50":121.0732930801347,"p95":988.1247487201446,"p99":1061.9745296637977}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:54+00","p50":165.4251362172549,"p95":928.4294835343946,"p99":942.6991447938342}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:55+00","p50":211.54403094618056,"p95":866.8777721974446,"p99":876.0741287995039}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:56+00","p50":148.39759754547617,"p95":1021.2213105553435,"p99":1040.6437717656609}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:57+00","p50":202.440413042906,"p95":1032.3409208176577,"p99":1063.3210063817955}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:58+00","p50":126.1553333929845,"p95":952.7591246429703,"p99":959.3141497077312}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:55:59+00","p50":106.04003464074076,"p95":912.8526685361825,"p99":931.6941608565618}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:00+00","p50":175.20829748903174,"p95":902.5486815882707,"p99":921.7826411930952}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:01+00","p50":123.46824211458333,"p95":913.9455413368282,"p99":946.1337241536828}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:02+00","p50":99.97574845833333,"p95":895.5412103050807,"p99":918.4313967901328}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:03+00","p50":119.80208308584808,"p95":896.4513558697473,"p99":913.2645265252166}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:04+00","p50":136.08803774999998,"p95":890.009967151612,"p99":902.0250404852159}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:05+00","p50":113.95570694711539,"p95":823.8663076288238,"p99":846.5872217292182}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:06+00","p50":258.37817738327266,"p95":853.3923024078364,"p99":870.9319858717784}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:07+00","p50":72.19650545777777,"p95":849.4139139104005,"p99":875.0250962306997}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:08+00","p50":161.45053325079363,"p95":919.9695436057938,"p99":927.0848874578335}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:09+00","p50":200.71569911760355,"p95":1004.786501879834,"p99":1012.6923325638595}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:10+00","p50":192.25228730500456,"p95":982.7831853085806,"p99":1003.2136844006201}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:11+00","p50":190.42256489002693,"p95":889.4916020107272,"p99":906.7136873276917}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:12+00","p50":180.53186151909722,"p95":946.1449973251665,"p99":959.1240089580954}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:13+00","p50":104.36899926020409,"p95":969.4872270957368,"p99":996.1314499955076}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:14+00","p50":183.29227608486983,"p95":945.2271272888844,"p99":993.2516379678341}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:15+00","p50":176.37477620889894,"p95":936.0834071160306,"p99":967.4712838606898}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:16+00","p50":186.42903416409038,"p95":916.8379949791525,"p99":928.6747990717902}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:17+00","p50":156.57689333179488,"p95":871.2586082098647,"p99":887.7203504018418}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:18+00","p50":114.64162365451388,"p95":895.2971975759209,"p99":903.0477690164037}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:19+00","p50":235.04740077333332,"p95":872.8779647409461,"p99":889.7656913739562}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:20+00","p50":153.74384801754388,"p95":843.9760518628112,"p99":852.5872082839335}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:21+00","p50":152.23202133854167,"p95":955.1071589400915,"p99":962.5511349698356}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:22+00","p50":126.83337179833332,"p95":909.9633355668569,"p99":923.81586669412}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:23+00","p50":158.1827645,"p95":888.7884191575698,"p99":895.8356720055508}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:24+00","p50":132.48452823305593,"p95":921.8749021593354,"p99":939.1610657758614}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:25+00","p50":121.51101007747747,"p95":849.422576869619,"p99":872.3421843770702}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:26+00","p50":116.48542109270834,"p95":997.8603419677481,"p99":1017.5134794563093}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:27+00","p50":165.8858623706235,"p95":964.9446332949128,"p99":980.6782928025386}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:28+00","p50":168.84646393124999,"p95":880.7891151854892,"p99":895.442993368261}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:29+00","p50":138.04189809602042,"p95":888.5264704938068,"p99":911.4266876194215}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:30+00","p50":109.6970434037461,"p95":872.5494232015429,"p99":892.2464995572557}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:31+00","p50":134.31282517701152,"p95":870.289858334569,"p99":883.4895910759373}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:32+00","p50":152.40456505381945,"p95":860.0831595744012,"p99":867.8893086808172}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:33+00","p50":246.33952829737729,"p95":785.8311239026597,"p99":808.1832017985204}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:34+00","p50":124.7573568802297,"p95":849.61527544969,"p99":856.7323384005792}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:35+00","p50":179.17429046191407,"p95":808.104439822826,"p99":816.7052037120468}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:36+00","p50":151.33991941434422,"p95":817.3983090804626,"p99":840.6604697154046}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:37+00","p50":122.39671626583335,"p95":842.4354749725806,"p99":851.0335445852234}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:38+00","p50":183.35875861830064,"p95":873.9560709972175,"p99":896.2622264609871}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:39+00","p50":171.60858856770835,"p95":886.9345350021864,"p99":898.9414649667574}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:40+00","p50":117.65405300676376,"p95":826.1091767647613,"p99":838.5779546571117}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:41+00","p50":250.26664968761906,"p95":886.4128486206162,"p99":895.130060287492}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:42+00","p50":116.53911178472224,"p95":870.8469275792831,"p99":884.6443679268099}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:43+00","p50":133.97024570814816,"p95":866.175323951934,"p99":876.7401782512936}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:44+00","p50":211.89453912666664,"p95":871.0439476787909,"p99":879.2490609106138}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:45+00","p50":165.48187257772713,"p95":858.5737040917704,"p99":875.2543664990842}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:46+00","p50":190.08682299002447,"p95":870.6147952286188,"p99":877.347770909992}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:47+00","p50":157.52544811215304,"p95":824.7689720100651,"p99":846.0025277634237}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:48+00","p50":143.09608598000003,"p95":884.9612304658546,"p99":916.741796155164}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:49+00","p50":164.6824527998047,"p95":845.8190653579314,"p99":855.5177646531343}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:50+00","p50":180.08528551300816,"p95":795.4304536331525,"p99":822.7067233357036}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:51+00","p50":155.5996369375,"p95":826.8112501900187,"p99":846.7379960078048}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:52+00","p50":197.06135132823619,"p95":833.4034900426662,"p99":843.5824958454839}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:53+00","p50":160.95806635652278,"p95":825.121729910877,"p99":836.7425346373199}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:54+00","p50":190.7185328839286,"p95":951.2571217422601,"p99":963.5951839026613}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:55+00","p50":191.6159233537415,"p95":881.0169029542449,"p99":896.757137292236}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:56+00","p50":202.76030510559997,"p95":978.8576153465106,"p99":987.2800546537131}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:57+00","p50":177.64472133928572,"p95":983.7039414734202,"p99":991.7613911467065}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:58+00","p50":197.72491880999996,"p95":958.7936988208291,"p99":965.8986660894025}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:56:59+00","p50":192.40619392,"p95":942.6542469350187,"p99":954.2171187332382}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:00+00","p50":209.00942781789794,"p95":846.7592319056391,"p99":876.5895930640573}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:01+00","p50":226.35854317948716,"p95":773.7326322991071,"p99":783.8758123231243}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:02+00","p50":196.09093422096188,"p95":846.4099964046095,"p99":867.3811477254403}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:03+00","p50":149.61345480208334,"p95":884.0632741746203,"p99":903.5188660699201}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:04+00","p50":176.3433206612903,"p95":858.37015681649,"p99":865.9472774573711}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:05+00","p50":169.27651598632653,"p95":838.1024160095966,"p99":854.8060181197125}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:06+00","p50":167.86956283018867,"p95":843.737486599003,"p99":865.4133259899307}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:07+00","p50":119.87128898090278,"p95":830.5845048423324,"p99":848.8686716651413}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:08+00","p50":194.78137441452992,"p95":854.4761718368759,"p99":861.1286121662312}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:09+00","p50":183.47015151195012,"p95":840.9179911538796,"p99":858.5101744739287}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:10+00","p50":173.78004015037592,"p95":912.5501245376392,"p99":939.0030638039458}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:11+00","p50":220.35982033230994,"p95":1020.8147795245387,"p99":1079.2313312269969}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:12+00","p50":205.478724265928,"p95":495.73268096338603,"p99":650.28912377063}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:13+00","p50":136.91599256,"p95":275.66034282469826,"p99":342.84942917316}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:14+00","p50":176.59797973898134,"p95":338.3322017870227,"p99":445.15826012586734}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:15+00","p50":294.95172765140484,"p95":579.9882105626388,"p99":588.0058521373022}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:16+00","p50":269.7881536822917,"p95":593.5481800906277,"p99":623.3601852068497}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:17+00","p50":191.61235888489796,"p95":680.1936659421061,"p99":690.4271267899786}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:18+00","p50":127.62022661284722,"p95":821.4532480339763,"p99":838.8462437732536}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:19+00","p50":255.8771398249578,"p95":806.8402045638529,"p99":817.0139567688144}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:20+00","p50":108.71228258333333,"p95":741.8602089435624,"p99":758.3475018707709}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:21+00","p50":254.48344459612244,"p95":752.503049122378,"p99":758.8613864332516}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:22+00","p50":422.8563147959184,"p95":734.8527514590472,"p99":747.9503598134165}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:23+00","p50":162.60151670833332,"p95":763.5756034414821,"p99":785.115600406538}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:24+00","p50":116.28094488133335,"p95":882.4578937072927,"p99":895.3161305167309}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:25+00","p50":170.92700816748768,"p95":907.2997351931609,"p99":930.5189453282621}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:26+00","p50":200.76954549759998,"p95":957.9189092632095,"p99":974.0073712633152}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:27+00","p50":180.30882015933415,"p95":954.8774422871943,"p99":963.6324726340258}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:28+00","p50":190.29681368560003,"p95":913.0914094180407,"p99":925.369113491051}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:29+00","p50":209.59770553846153,"p95":841.5732106839988,"p99":870.3409822032706}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:30+00","p50":125.04656907237762,"p95":836.6642267289651,"p99":843.5996865760193}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:31+00","p50":214.58123212666666,"p95":861.6391432572482,"p99":875.3648603105014}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:32+00","p50":139.17308868749998,"p95":855.1284989433086,"p99":863.9141369260052}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:33+00","p50":175.98525805098856,"p95":861.2041798930019,"p99":869.2832591140433}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:34+00","p50":154.026684078125,"p95":862.3629626903114,"p99":874.901503602491}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:35+00","p50":122.2275562606305,"p95":819.6602614730248,"p99":837.7289479879579}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:36+00","p50":175.77455474246648,"p95":847.8733144722669,"p99":853.4903827631224}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:37+00","p50":145.39856163599995,"p95":877.2027876840642,"p99":885.1823409265546}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:38+00","p50":138.2663230025271,"p95":916.6997914615837,"p99":939.5589644648153}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:39+00","p50":153.74529620000007,"p95":918.2758165777407,"p99":942.8847859748977}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:40+00","p50":186.88589289197282,"p95":840.6811296202233,"p99":851.6739178813743}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:41+00","p50":167.5479781168,"p95":884.2164359735932,"p99":892.9634028791384}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:42+00","p50":158.18577194776023,"p95":874.82629078687,"p99":893.6050623009086}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:43+00","p50":152.59206525390627,"p95":833.2465667532628,"p99":844.582977924428}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:44+00","p50":144.18717206399998,"p95":811.7708844612529,"p99":821.6937596285651}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:45+00","p50":88.59937552256945,"p95":841.6914485754058,"p99":849.8090939927498}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:46+00","p50":125.47696279063362,"p95":871.3211072251275,"p99":888.080922826032}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:47+00","p50":130.5801814575657,"p95":766.5967676926804,"p99":788.2749662900662}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:48+00","p50":122.30350392,"p95":817.5356144203738,"p99":827.2611394484458}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:49+00","p50":197.54994611520004,"p95":837.75174851437,"p99":855.250270595908}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:50+00","p50":149.92793068965514,"p95":896.8628763791648,"p99":909.4217881651276}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:51+00","p50":119.29048119120002,"p95":870.7710199900783,"p99":878.2678004304773}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:52+00","p50":133.0596855935374,"p95":898.3808490170554,"p99":906.3611578563001}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:53+00","p50":78.1036478768,"p95":876.9940212008638,"p99":888.5417221689404}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:54+00","p50":239.85090760512318,"p95":890.0534791850829,"p99":900.7907516783876}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:55+00","p50":322.2759638616,"p95":835.2476039414776,"p99":847.5490980254204}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:56+00","p50":233.57189477777777,"p95":891.309223294257,"p99":905.8114425563447}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:57+00","p50":169.87541429119045,"p95":903.5470123792835,"p99":916.1445691046008}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:58+00","p50":230.2200012382019,"p95":950.1423541921979,"p99":963.3216403252123}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:57:59+00","p50":158.77989038157892,"p95":882.9414618849436,"p99":920.4623596481201}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:00+00","p50":205.47255604756242,"p95":864.7277724766088,"p99":875.8033505347252}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:01+00","p50":182.19201718186338,"p95":970.4912706228339,"p99":978.899355041014}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:02+00","p50":146.54740160132474,"p95":982.1943984687033,"p99":997.9344947708964}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:03+00","p50":145.79797560831642,"p95":931.4359231342376,"p99":941.364305348619}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:04+00","p50":178.9156280873303,"p95":852.6668826484529,"p99":884.3968046420513}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:05+00","p50":186.98989780000002,"p95":855.6845672966872,"p99":865.4199532000429}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:06+00","p50":183.8177053804511,"p95":881.6532832001445,"p99":890.7612112305958}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:07+00","p50":136.82893600454844,"p95":890.4584673262835,"p99":903.8645662256082}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:08+00","p50":163.81014588612842,"p95":901.2985902214198,"p99":907.6775247981661}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:09+00","p50":197.11993265329272,"p95":879.4619425179798,"p99":894.737848644577}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:10+00","p50":125.97342526340579,"p95":883.2473868068271,"p99":897.6481999101736}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:11+00","p50":108.39121463258027,"p95":864.0428543421314,"p99":896.6223967749221}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:12+00","p50":215.04603716666665,"p95":934.2043241575767,"p99":939.2503676995491}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:13+00","p50":194.0505863819444,"p95":850.2607882541355,"p99":863.7357129003115}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:14+00","p50":207.4327120133929,"p95":763.3101262636,"p99":793.2583906605608}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:15+00","p50":187.63962730697193,"p95":787.2009096089149,"p99":822.3540363000163}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:16+00","p50":188.7290316278033,"p95":860.569369492955,"p99":869.2586306507238}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:17+00","p50":163.88265877638406,"p95":756.953459420196,"p99":816.4881557530148}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:18+00","p50":178.28135543652448,"p95":794.0716570072933,"p99":818.3780319736439}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:19+00","p50":184.0868202393162,"p95":786.0849312154867,"p99":799.1293262856591}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:20+00","p50":136.04033266560344,"p95":819.1361490242815,"p99":832.3235076869158}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:21+00","p50":139.0703959883333,"p95":831.7969277173427,"p99":840.3361013864928}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:22+00","p50":147.6799558290344,"p95":869.4908928305643,"p99":883.4328006645358}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:23+00","p50":162.77030735839847,"p95":831.118559624686,"p99":840.4751780910248}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:24+00","p50":170.9109675,"p95":818.0868503546485,"p99":834.5299188729825}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:25+00","p50":150.24318677567666,"p95":855.5146162084574,"p99":889.6101004106955}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:26+00","p50":148.4900774981685,"p95":908.3268351238542,"p99":921.9338256317518}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:27+00","p50":156.5845943836937,"p95":895.5094812551873,"p99":904.904236611205}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:28+00","p50":209.91834743277778,"p95":895.2443964638712,"p99":915.8609933364766}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:29+00","p50":201.18523922727275,"p95":732.7740082200005,"p99":790.2765379351217}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:30+00","p50":168.22008065867348,"p95":787.0791975608491,"p99":805.5778842861208}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:31+00","p50":170.62990430555556,"p95":811.4747320580408,"p99":827.4329608339763}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:32+00","p50":164.7955307677871,"p95":893.2400488897391,"p99":901.840320710366}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:33+00","p50":186.6143432916667,"p95":816.0849116355928,"p99":842.157773888579}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:34+00","p50":245.47030793482443,"p95":792.9606383549976,"p99":807.4970388454868}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:35+00","p50":86.64443295312499,"p95":748.1502222607772,"p99":766.7270593315138}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:36+00","p50":184.0029771568,"p95":795.4702404007363,"p99":802.8528177571917}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:37+00","p50":193.74370665624997,"p95":816.1850115301278,"p99":839.9968683440061}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:38+00","p50":117.46164822916667,"p95":806.5076188906088,"p99":814.6575669835253}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:39+00","p50":93.6755528734568,"p95":780.6095743153526,"p99":809.7917936112352}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:40+00","p50":202.41578812799165,"p95":851.4989380451063,"p99":865.4644753697632}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:41+00","p50":175.23996636083984,"p95":810.3523624125195,"p99":817.7454747586603}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:42+00","p50":178.30670065219977,"p95":873.3742811842861,"p99":896.8632570604782}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:43+00","p50":169.30451451220702,"p95":897.1472470179737,"p99":920.8346847103531}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:44+00","p50":169.70924694220506,"p95":806.7907398891178,"p99":829.2907950970688}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:45+00","p50":141.05337964583333,"p95":842.3391232886923,"p99":849.7472760466477}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:46+00","p50":174.42800192039542,"p95":832.8985566732122,"p99":844.6474988681371}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:47+00","p50":158.91901319281527,"p95":819.9688427197042,"p99":832.3769665671491}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:48+00","p50":187.39900745089287,"p95":889.8685494240916,"p99":915.7365428901035}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:49+00","p50":198.8938386181536,"p95":899.445367401646,"p99":907.588925046711}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:50+00","p50":151.03853918758062,"p95":850.5373120098733,"p99":857.5200552733079}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:51+00","p50":190.30611564646463,"p95":945.1492518236959,"p99":952.7547030836496}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:52+00","p50":187.2169048148148,"p95":946.4519159507673,"p99":955.293379503597}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:53+00","p50":175.90747164086955,"p95":873.6018164776292,"p99":943.9422805913607}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:54+00","p50":129.1028387429761,"p95":872.7295716829356,"p99":884.3039437253153}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:55+00","p50":173.21547338836368,"p95":835.6733723349938,"p99":853.8553608019085}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:56+00","p50":137.58425940921055,"p95":950.7241558504247,"p99":963.8412872506565}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:57+00","p50":141.60105837062196,"p95":930.3812511059479,"p99":961.1611363871963}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:58+00","p50":128.86066387424643,"p95":873.9101534742111,"p99":888.1607073235281}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:58:59+00","p50":145.30552528564456,"p95":833.5805109523371,"p99":874.1262407595099}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:00+00","p50":193.2733399301758,"p95":797.6968331355337,"p99":805.0915330568716}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:01+00","p50":179.48874559044287,"p95":844.6777408906374,"p99":863.2847411684404}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:02+00","p50":183.09237122222223,"p95":880.7160970357564,"p99":893.8124545879597}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:03+00","p50":167.00132923725286,"p95":898.4135355837287,"p99":911.4005361591181}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:04+00","p50":102.07949337147934,"p95":844.984319999935,"p99":855.2633889723281}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:05+00","p50":203.67906000141065,"p95":884.4545690916589,"p99":895.8763134296615}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:06+00","p50":178.7640719927584,"p95":841.6313163885368,"p99":848.4726919600242}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:07+00","p50":126.54731406436012,"p95":828.4113244449186,"p99":839.730397782313}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:08+00","p50":184.83625472500003,"p95":847.6811902639579,"p99":858.0294077421712}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:09+00","p50":162.7282378758329,"p95":813.1278536521816,"p99":823.941843023924}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:10+00","p50":184.09187397222226,"p95":826.9066767554041,"p99":835.8755076233817}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:11+00","p50":143.30316259342825,"p95":839.9979488016279,"p99":864.4450456742356}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:12+00","p50":145.26779916190478,"p95":870.1439908023989,"p99":880.2305444206679}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:13+00","p50":183.17740040160004,"p95":827.0452374428324,"p99":837.4688403663146}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:14+00","p50":157.51727664080002,"p95":864.0707099732838,"p99":875.4966159079574}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:15+00","p50":86.98427658941799,"p95":862.0429723665591,"p99":869.2428887539352}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:16+00","p50":98.44034057591037,"p95":789.1675527030794,"p99":798.5008939785268}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:17+00","p50":341.1590634348958,"p95":767.9068714420106,"p99":788.4525194337514}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:18+00","p50":241.82289973566958,"p95":848.1582413059695,"p99":856.098864116317}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:19+00","p50":203.01533237632657,"p95":834.5234562518876,"p99":839.7597485047926}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:20+00","p50":167.318747591453,"p95":875.6351779696748,"p99":914.3276564083385}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:21+00","p50":159.73527463319456,"p95":805.3486562910022,"p99":818.734330477255}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:22+00","p50":172.9648792369792,"p95":834.7903505782507,"p99":848.68350778252}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:23+00","p50":203.42755129726518,"p95":863.1568438167423,"p99":877.8075717720233}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:24+00","p50":119.76454815833333,"p95":819.3168912744645,"p99":854.7370891484218}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:25+00","p50":105.67013907881774,"p95":844.1550114371091,"p99":850.7362793500367}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:26+00","p50":148.80390642857142,"p95":926.2793256835982,"p99":935.4577087710404}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:27+00","p50":142.56052870880382,"p95":931.8305482115695,"p99":941.5639298426314}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:28+00","p50":110.54339103855445,"p95":880.2988504818796,"p99":885.424539604326}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:29+00","p50":119.34372194833334,"p95":842.9952818366511,"p99":861.7519902090243}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:30+00","p50":153.36206650555556,"p95":849.5015131749806,"p99":859.5135552050888}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:31+00","p50":165.24902673841643,"p95":787.0533032587646,"p99":797.0238449536274}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:32+00","p50":115.31395963324063,"p95":825.1446394616305,"p99":836.6648275119625}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:33+00","p50":112.281932736,"p95":817.3780170918726,"p99":847.4041249760129}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:34+00","p50":302.5280567333334,"p95":842.198347323976,"p99":856.9588916265504}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:35+00","p50":247.92020849218753,"p95":803.3848005731365,"p99":820.2798167367154}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:36+00","p50":232.8413877106383,"p95":814.7907499701146,"p99":822.386154287454}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:37+00","p50":101.47158743229166,"p95":816.2272667660048,"p99":829.8157160039689}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:38+00","p50":246.95254437270737,"p95":868.3457715635427,"p99":885.4114586917528}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:39+00","p50":193.32054050000002,"p95":877.615584096091,"p99":887.8130802712661}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:40+00","p50":212.28628286812003,"p95":787.5661364529963,"p99":802.7144315908237}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:41+00","p50":182.5573922013889,"p95":811.030653572247,"p99":822.308468364398}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:42+00","p50":103.2344727317073,"p95":920.5319954097365,"p99":936.2681185472651}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:43+00","p50":114.68404620268734,"p95":880.5047412017004,"p99":916.0964171762315}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:44+00","p50":168.25517569323114,"p95":872.8371361305415,"p99":882.6285299837574}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:45+00","p50":175.12033912434964,"p95":845.75189753443,"p99":859.2601783423245}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:46+00","p50":134.3313430982456,"p95":829.3129302591901,"p99":839.0924018305377}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:47+00","p50":122.07750859408536,"p95":819.4737569617585,"p99":836.3736084941319}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:48+00","p50":317.67966964816713,"p95":807.61641441701,"p99":817.785603950816}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:49+00","p50":186.39362001345455,"p95":804.5687614234264,"p99":819.6361225594361}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:50+00","p50":230.02951356593408,"p95":784.9809581777502,"p99":793.7321683183965}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:51+00","p50":191.20990977764566,"p95":786.1663352210547,"p99":797.8370661570744}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:52+00","p50":318.8493388704475,"p95":798.9786475373273,"p99":805.1812458698506}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:53+00","p50":296.95293434,"p95":804.0860942388537,"p99":827.7717379127273}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:54+00","p50":225.11894616683088,"p95":849.6645626382929,"p99":864.2483351395965}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:55+00","p50":260.5254097216,"p95":844.8522431216563,"p99":852.3921021334133}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:56+00","p50":310.87405158680554,"p95":806.7351666863606,"p99":819.4636672896631}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:57+00","p50":220.30600520911565,"p95":835.1535979836284,"p99":871.4866122278152}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:58+00","p50":109.63955068749999,"p95":881.565866809257,"p99":892.0055733020889}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 09:59:59+00","p50":198.15537455999998,"p95":840.69227094511,"p99":851.283663608774}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:00+00","p50":166.11430560215948,"p95":918.7865475653692,"p99":930.2192566115667}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:01+00","p50":162.17676441095236,"p95":1014.2086430172969,"p99":1033.2057017093318}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:02+00","p50":192.16365939188898,"p95":986.4037805080756,"p99":1008.7898510850499}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:03+00","p50":190.39577933107557,"p95":900.6057002146898,"p99":908.2778550839284}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:04+00","p50":183.4991067015458,"p95":911.0205149664984,"p99":926.8962829827192}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:05+00","p50":176.6736173312808,"p95":902.8057805813105,"p99":931.6929749787251}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:06+00","p50":198.79067889886034,"p95":969.3768985383729,"p99":981.7203422413281}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:07+00","p50":135.07101200404762,"p95":927.1700270743127,"p99":949.1835467930374}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:08+00","p50":153.75789837279999,"p95":1045.8361207445917,"p99":1068.6121666014935}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:09+00","p50":116.56865983636364,"p95":1024.874783253324,"p99":1039.080510576409}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:10+00","p50":71.38067071740959,"p95":919.5608822553487,"p99":932.3591493821476}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:11+00","p50":105.28218102503402,"p95":874.0818799082451,"p99":909.2175779759668}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:12+00","p50":295.20931178819444,"p95":899.5845465791394,"p99":908.0865533985705}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:13+00","p50":262.1222675892857,"p95":911.4484009817276,"p99":918.3550241717003}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:14+00","p50":271.0996091909722,"p95":846.6616170113145,"p99":885.2126554358765}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:15+00","p50":242.57055588217054,"p95":879.5132677591273,"p99":887.6251390450956}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:16+00","p50":204.53184430227267,"p95":861.735739463106,"p99":870.2040405410114}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:17+00","p50":112.33673290480002,"p95":873.6258303480015,"p99":881.3585451542325}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:18+00","p50":96.9542270883117,"p95":930.0219748612698,"p99":935.802161594565}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:19+00","p50":103.952930185,"p95":935.1480798663462,"p99":942.7340183543563}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:20+00","p50":180.64690076275505,"p95":922.654272329541,"p99":938.0034936966453}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:21+00","p50":228.76896540057973,"p95":1026.513287539627,"p99":1036.598872175405}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:22+00","p50":218.84027590471496,"p95":983.9368617904506,"p99":995.4888339378841}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:23+00","p50":175.81498074149656,"p95":860.5604616109833,"p99":879.7731037697635}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:24+00","p50":168.4712555361328,"p95":809.311412737604,"p99":833.2062403127438}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:25+00","p50":179.84735103389832,"p95":821.5306782239012,"p99":837.5939285539642}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:26+00","p50":161.72503458823527,"p95":985.9087405614333,"p99":1007.433809488121}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:27+00","p50":144.51572759680005,"p95":936.8913438227207,"p99":971.7632364394567}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:28+00","p50":138.00443167498375,"p95":899.2940352987945,"p99":912.3564448851894}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:29+00","p50":120.8564962326389,"p95":867.3194896030179,"p99":880.5656574688746}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:30+00","p50":137.36096495959595,"p95":817.186764840102,"p99":826.8676097650881}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:31+00","p50":166.37622804578854,"p95":857.6917881250985,"p99":862.788531857689}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:32+00","p50":183.61637246632657,"p95":868.1809439607903,"p99":879.5642906291596}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:33+00","p50":138.0065252050781,"p95":833.2213823105787,"p99":869.0135340903515}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:34+00","p50":179.82331476133464,"p95":892.3295770416847,"p99":896.728501797911}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:35+00","p50":153.62195281836054,"p95":884.2548940876987,"p99":909.0003183440796}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:36+00","p50":150.7200812099419,"p95":850.0443118929558,"p99":883.3695515053863}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:37+00","p50":160.727275975026,"p95":848.3011523341111,"p99":855.9132755139594}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:38+00","p50":141.9466246206597,"p95":876.2962226369677,"p99":891.7115419506276}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:39+00","p50":140.82421233120002,"p95":921.5521378388069,"p99":932.0296096747934}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:40+00","p50":141.5397631806667,"p95":876.3638062240569,"p99":893.7116229276454}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:41+00","p50":178.89385325713593,"p95":853.3393173404262,"p99":865.0178381588537}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:42+00","p50":188.30476236236564,"p95":849.6291273746075,"p99":862.054568195266}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:43+00","p50":160.40736885294115,"p95":860.240026136246,"p99":866.392405582463}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:44+00","p50":203.333769375,"p95":822.9328197547151,"p99":866.7030102276223}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:45+00","p50":193.9950418562271,"p95":773.1466798052774,"p99":784.9713832936586}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:46+00","p50":287.89926485432784,"p95":723.839274095176,"p99":735.9406335057557}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:47+00","p50":150.6347909192708,"p95":742.6267299572794,"p99":766.8737555409022}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:48+00","p50":209.65199923873305,"p95":840.1415456347434,"p99":851.4754367493546}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:49+00","p50":136.9707566947349,"p95":843.7665734303041,"p99":849.3933329273966}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:50+00","p50":94.12378794579399,"p95":825.1858070932616,"p99":835.2019341712507}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:51+00","p50":197.6621836195006,"p95":899.9844136965373,"p99":907.4373899918642}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:52+00","p50":174.8607532465278,"p95":941.8060756854726,"p99":959.7602340379493}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:53+00","p50":296.42514584895827,"p95":928.7739933253129,"p99":936.2445124620166}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:54+00","p50":227.17513267823858,"p95":926.5112004296053,"p99":939.9419139842147}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:55+00","p50":196.72351287691328,"p95":818.5751321330375,"p99":880.4050721598891}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:56+00","p50":235.05400658159724,"p95":1035.7073773303491,"p99":1048.329461471053}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:57+00","p50":163.96600068842366,"p95":990.1467022744823,"p99":1003.1244354150537}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:58+00","p50":153.19319234566325,"p95":914.5330726429173,"p99":938.0433091333896}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:00:59+00","p50":162.92828870138888,"p95":820.2416148921276,"p99":830.9539077754177}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:00+00","p50":159.83601,"p95":844.7629886336921,"p99":856.5085628993781}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:01+00","p50":116.63129795525495,"p95":882.609295516597,"p99":896.6406561951694}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:02+00","p50":187.72468612637638,"p95":853.7046334776056,"p99":863.324613534769}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:03+00","p50":172.05119118836808,"p95":793.8851101734182,"p99":808.6299718432786}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:04+00","p50":126.1386016752,"p95":844.7611095208542,"p99":857.6965635541605}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:05+00","p50":99.36498687854542,"p95":835.5188055285263,"p99":845.966431916355}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:06+00","p50":151.7226567620945,"p95":908.23365320242,"p99":926.0945411584297}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:07+00","p50":151.07609695600001,"p95":856.0808087424689,"p99":874.1529205263485}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:08+00","p50":143.52825936753982,"p95":858.4406746239306,"p99":880.4231490434425}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:09+00","p50":126.25606673958333,"p95":880.0814778097607,"p99":890.6881999509126}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:10+00","p50":168.56095862339637,"p95":837.7860569285593,"p99":851.7639510759705}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:11+00","p50":151.42913555808082,"p95":834.3154321234775,"p99":850.9892860555507}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:12+00","p50":197.67394795370367,"p95":895.1241188123616,"p99":906.9121400381731}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:13+00","p50":146.8564646670135,"p95":857.6746972414066,"p99":876.7153556077046}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:14+00","p50":173.7467032181436,"p95":851.6507518743263,"p99":874.503578248196}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:15+00","p50":181.02385869044988,"p95":817.9801352288806,"p99":826.4891468908093}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:16+00","p50":163.7463807126736,"p95":861.7405327012272,"p99":871.7777113740863}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:17+00","p50":150.61434848232489,"p95":848.1660619164966,"p99":860.7749507194861}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:18+00","p50":274.1729599806094,"p95":1048.388899568616,"p99":1122.1540471736728}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:19+00","p50":276.63421335566034,"p95":772.6997212332265,"p99":983.6065466612949}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:20+00","p50":244.382824505042,"p95":536.6031154590626,"p99":551.4653246493314}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:21+00","p50":296.0571388454221,"p95":592.5330621953955,"p99":603.0409826372255}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:22+00","p50":300.7055617161172,"p95":581.9584651852905,"p99":596.9479401912281}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:23+00","p50":252.690763941736,"p95":615.6074920214504,"p99":693.8182505832008}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:24+00","p50":197.34981401809523,"p95":848.7858471430435,"p99":872.9305040788779}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:25+00","p50":139.41271283860516,"p95":886.0077513376368,"p99":897.4945708632462}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:26+00","p50":86.10661713585434,"p95":964.0182443360743,"p99":979.5888408579325}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:27+00","p50":105.15804421485412,"p95":973.5923539102548,"p99":983.8275524321679}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:28+00","p50":125.04745997600001,"p95":904.1082631772285,"p99":926.4747735242744}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:29+00","p50":100.63362903540668,"p95":918.3684092371317,"p99":925.5224549313008}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:30+00","p50":286.29109053499997,"p95":863.5388040811893,"p99":873.2606452301026}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:31+00","p50":296.3668361626518,"p95":812.9605303177253,"p99":831.4795828648829}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:32+00","p50":128.38823052333333,"p95":904.5687262525835,"p99":911.3207109309974}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:33+00","p50":131.09207954239997,"p95":889.8076267234786,"p99":914.7728421893523}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:34+00","p50":136.70096529947918,"p95":903.2217258891269,"p99":924.4831817087618}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:35+00","p50":171.84946997222224,"p95":947.1508902771726,"p99":955.7933080648321}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:36+00","p50":104.0070751611111,"p95":897.4921456905136,"p99":949.3497791284091}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:37+00","p50":139.86422258777174,"p95":850.712282487331,"p99":865.5232610244515}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:38+00","p50":200.15644740714285,"p95":908.2005096783304,"p99":915.1984964564455}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:39+00","p50":169.9142976598959,"p95":902.9373398899021,"p99":908.3350897891681}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:40+00","p50":165.06718408333336,"p95":845.6421121253114,"p99":865.7776205245923}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:41+00","p50":112.46411188341695,"p95":842.2324015774147,"p99":849.1387556997344}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:42+00","p50":115.29476190239998,"p95":874.7358942472237,"p99":889.6599273632157}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:43+00","p50":194.60808470833334,"p95":863.2310296919175,"p99":873.5266265561834}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:44+00","p50":162.18725555293068,"p95":835.390795330403,"p99":848.9965164217439}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:45+00","p50":154.93717521198155,"p95":825.9405708597988,"p99":837.4292871431062}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:46+00","p50":108.10904065442178,"p95":831.6093827216499,"p99":851.3649594390047}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:47+00","p50":137.03825264739436,"p95":885.4721382976873,"p99":899.724042890026}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:48+00","p50":180.46832732758622,"p95":912.9927034540376,"p99":920.8621061893286}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:49+00","p50":159.5275636968,"p95":918.1454139500954,"p99":925.7259328867649}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:50+00","p50":122.16703512490001,"p95":912.8453897204832,"p99":954.8069171410622}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:51+00","p50":227.77388282100588,"p95":1019.0018436019933,"p99":1041.292964826922}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:52+00","p50":220.1572919344,"p95":973.156459517609,"p99":984.2698333321151}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:53+00","p50":166.7768944736,"p95":908.9079800448225,"p99":925.1644327057546}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:54+00","p50":123.56298205954452,"p95":898.3022433690035,"p99":911.7325732858723}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:55+00","p50":159.849025725327,"p95":852.2952049075841,"p99":875.5115848493364}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:56+00","p50":188.12265944822482,"p95":1016.8605849140257,"p99":1041.6096746759044}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:57+00","p50":183.74680940317458,"p95":1014.7667152046969,"p99":1030.1115887943802}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:58+00","p50":191.03585399107143,"p95":1016.27459908152,"p99":1037.7787280171804}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:01:59+00","p50":169.99458024,"p95":907.8590383908859,"p99":951.8549436244683}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:00+00","p50":193.26178703015873,"p95":909.5027932532522,"p99":918.4499019041493}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:01+00","p50":190.49572352184381,"p95":933.7192719066219,"p99":940.0362954966869}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:02+00","p50":148.1944754114583,"p95":884.78498460134,"p99":912.0111781699621}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:03+00","p50":174.81503957368423,"p95":869.2746602184376,"p99":877.9269009095797}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:04+00","p50":168.10735484258768,"p95":881.3677300578006,"p99":895.6804625326466}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:05+00","p50":153.46120382079997,"p95":835.9406996298186,"p99":842.6451251293204}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:06+00","p50":162.6076224810402,"p95":881.059172603003,"p99":900.7386347860162}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:07+00","p50":151.88297088351646,"p95":896.8716963471205,"p99":916.2068329394014}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:08+00","p50":170.5102113051948,"p95":904.3017320631044,"p99":918.0505297390706}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:09+00","p50":185.04685686802722,"p95":872.8469072901004,"p99":883.2415676645073}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:10+00","p50":87.77273757632653,"p95":832.1637463920467,"p99":861.9307853038163}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:11+00","p50":310.49525700591715,"p95":829.440018856914,"p99":845.7490791495942}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:12+00","p50":102.61586903956042,"p95":817.2945834821236,"p99":833.203292625105}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:13+00","p50":229.4896077787047,"p95":843.7609774101055,"p99":860.7073175104742}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:14+00","p50":314.80678006936813,"p95":803.8028747423801,"p99":825.6630890226468}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:15+00","p50":82.41043383142856,"p95":867.5250060241204,"p99":876.7177022046893}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:16+00","p50":150.47148308085613,"p95":942.8140688354988,"p99":958.2866953739406}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:17+00","p50":155.7839442147847,"p95":833.4488585474522,"p99":887.587683554968}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:18+00","p50":100.9913765833333,"p95":872.1365243566116,"p99":883.3598888816058}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:19+00","p50":178.39304114444445,"p95":867.6117278561477,"p99":888.1925782782798}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:20+00","p50":151.8507375655769,"p95":783.8065772469182,"p99":810.5022761433102}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:21+00","p50":195.24753836608835,"p95":878.4994051102573,"p99":890.3637391389911}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:22+00","p50":163.63956541200952,"p95":910.9270911369466,"p99":922.2417381108133}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:23+00","p50":164.1877043121693,"p95":919.2270811614468,"p99":937.6262951172872}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:24+00","p50":127.7617623178862,"p95":944.0016841170265,"p99":972.8023039054885}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:25+00","p50":149.38074771700357,"p95":897.0265983509571,"p99":921.4845638812955}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:26+00","p50":244.97296553846152,"p95":898.4242808546873,"p99":936.5195142885536}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:27+00","p50":232.38026350951247,"p95":433.12515928521617,"p99":852.0792953501236}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:28+00","p50":172.3792037368421,"p95":301.40428674418183,"p99":347.62403543694074}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:29+00","p50":214.24099974982383,"p95":338.3528162475791,"p99":390.2594943625112}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:30+00","p50":418.2676236887755,"p95":597.5387754903724,"p99":744.7510864515519}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:31+00","p50":195.33941077806654,"p95":475.43554808602903,"p99":727.1423783208013}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:32+00","p50":221.65672155902777,"p95":598.0075381423155,"p99":609.3296569384065}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:33+00","p50":208.89651680145454,"p95":671.3071511502324,"p99":716.5039968436406}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:34+00","p50":114.81087264828305,"p95":791.0735256491943,"p99":817.6135529578929}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:35+00","p50":312.2080416244898,"p95":815.8439277841259,"p99":821.9153911498925}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:36+00","p50":281.34518132520805,"p95":820.0229808277415,"p99":840.0244576244454}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:37+00","p50":224.7577575712,"p95":851.0569414221504,"p99":863.2371527265549}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:38+00","p50":126.18491128000001,"p95":994.906050004869,"p99":1006.6767866526637}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:39+00","p50":160.27178630266937,"p95":970.6418437953681,"p99":1028.6779868023664}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:40+00","p50":129.28942839710407,"p95":876.0172597238575,"p99":896.6430675198844}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:41+00","p50":135.38810061588921,"p95":846.8487237049632,"p99":860.2510844307991}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:42+00","p50":92.91651636571429,"p95":889.463610041541,"p99":895.4463582454009}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:43+00","p50":104.15306932954545,"p95":870.0730674690545,"p99":878.2959397112976}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:44+00","p50":148.13785909311738,"p95":847.825427756708,"p99":859.4788230123806}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:45+00","p50":305.31134219757575,"p95":806.7053629610222,"p99":814.6163518680726}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:46+00","p50":274.760432584,"p95":799.4464475326027,"p99":823.4159073273785}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:47+00","p50":170.43918342514473,"p95":759.6282456654477,"p99":770.8109150501434}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:48+00","p50":228.67506637312945,"p95":751.1357782350208,"p99":757.9211683429842}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:49+00","p50":247.39521268166666,"p95":796.8003708406038,"p99":808.0777206419979}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:50+00","p50":71.95871031521739,"p95":851.4837643643057,"p99":863.0178567888167}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:51+00","p50":276.0358977329207,"p95":927.4869498040491,"p99":948.5766827600402}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:52+00","p50":92.96512248979592,"p95":958.2283383171816,"p99":978.6588916961688}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:53+00","p50":145.1813215030612,"p95":910.1184245023719,"p99":922.0425842349254}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:54+00","p50":292.70394327321424,"p95":895.4310942582346,"p99":913.5015485279479}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:55+00","p50":234.62894498800003,"p95":876.2759218531195,"p99":890.4247694156574}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:56+00","p50":99.53173216149068,"p95":1006.406793118706,"p99":1019.2086175818768}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:57+00","p50":340.10923925000003,"p95":947.4780001100912,"p99":965.0017143855654}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:58+00","p50":173.10884350487964,"p95":948.4195902042351,"p99":958.6355515503711}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:02:59+00","p50":133.5418786936102,"p95":884.1060314246583,"p99":901.6520684472365}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:00+00","p50":141.55644969959184,"p95":864.408010214092,"p99":874.425644606236}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:01+00","p50":130.46476850569377,"p95":988.1508854588895,"p99":1001.0893359296256}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:02+00","p50":141.48032447187927,"p95":1003.9474243657202,"p99":1009.9227158047997}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:03+00","p50":224.84583393910253,"p95":970.9113662847631,"p99":981.1308367691953}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:04+00","p50":192.78437462414263,"p95":973.172706024828,"p99":981.0666329842071}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:05+00","p50":186.93758796313915,"p95":928.9101315643018,"p99":947.0785502868301}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:06+00","p50":192.28184117003568,"p95":849.8637650377967,"p99":859.183244273796}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:07+00","p50":165.99276272569443,"p95":842.9926038506067,"p99":853.8836793816182}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:08+00","p50":173.09219820833334,"p95":968.4838752197277,"p99":1020.187609003779}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:09+00","p50":258.8496288333333,"p95":1056.1929971835634,"p99":1078.6969743246457}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:10+00","p50":211.9976353368056,"p95":944.5261879346649,"p99":956.364750284663}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:11+00","p50":169.1321328769728,"p95":883.4521334429955,"p99":894.263673265587}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:12+00","p50":128.64856468315972,"p95":876.7808633864831,"p99":882.7078971963919}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:13+00","p50":112.37337164462475,"p95":901.0310591847541,"p99":907.251405933084}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:14+00","p50":141.7946055326992,"p95":858.4254487800384,"p99":864.7971665242408}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:15+00","p50":208.85838206498084,"p95":849.4494675446998,"p99":859.3484745142731}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:16+00","p50":177.00135001442314,"p95":820.9889206170634,"p99":834.2865775464849}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:17+00","p50":175.9751774453125,"p95":885.4040462577453,"p99":892.3801483598402}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:18+00","p50":182.36291554239997,"p95":983.2330308080255,"p99":992.7433582526724}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:19+00","p50":253.41396019679996,"p95":1048.8213948840335,"p99":1058.2537654512428}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:20+00","p50":205.37926259260504,"p95":982.4137742199104,"p99":1018.9771433058169}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:21+00","p50":245.37304404813665,"p95":1041.9753787815944,"p99":1072.173609376647}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:22+00","p50":270.9472887009544,"p95":1068.444428020198,"p99":1081.322118370169}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:23+00","p50":219.3294581024,"p95":995.2890075863078,"p99":1004.8104113778741}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:24+00","p50":158.64078509375,"p95":1000.5851677772287,"p99":1011.046867790829}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:25+00","p50":134.30698569898885,"p95":936.7819268845432,"p99":958.7325013516953}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:26+00","p50":258.38619600879997,"p95":1012.9050302797722,"p99":1024.0639228883372}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:27+00","p50":254.51196537037035,"p95":929.3693892152722,"p99":959.3743782533484}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:28+00","p50":201.06333244444446,"p95":924.7578869597477,"p99":942.5362672310771}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:29+00","p50":187.86875107168737,"p95":929.0999319342017,"p99":936.414465574292}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:30+00","p50":193.8412544069317,"p95":905.5093446235935,"p99":925.2411994540195}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:31+00","p50":173.3479526584362,"p95":952.0201525772984,"p99":960.3930257425358}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:32+00","p50":179.5856648837209,"p95":953.0067878208844,"p99":973.4172354177185}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:33+00","p50":149.08312984375002,"p95":914.0971382130773,"p99":923.0711663391144}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:34+00","p50":231.62325369230769,"p95":974.3078081482352,"p99":989.3992107317554}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:35+00","p50":154.03314741145832,"p95":967.0832757022091,"p99":985.3361319287429}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:36+00","p50":127.80538507668118,"p95":981.4431058227528,"p99":990.05779714146}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:37+00","p50":108.73846880995475,"p95":954.0974545635856,"p99":966.8645034434672}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:38+00","p50":134.6252481990493,"p95":956.4189533201409,"p99":969.1062920474345}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:39+00","p50":162.63700930357143,"p95":957.3742065256375,"p99":966.8714038205532}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:40+00","p50":149.20997152256945,"p95":878.267058277296,"p99":918.7897698956596}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:41+00","p50":186.40341534911244,"p95":939.8879300867202,"p99":967.4674490571539}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:42+00","p50":169.95962970375456,"p95":998.2421924635918,"p99":1015.3642434044551}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:43+00","p50":107.57319514311686,"p95":879.0863165909961,"p99":886.9891254924684}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:44+00","p50":224.56639096730083,"p95":885.6019170524817,"p99":892.7491755234258}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:45+00","p50":194.22262114567673,"p95":879.5335813167306,"p99":886.863599378329}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:46+00","p50":205.7546216097436,"p95":904.9386220665613,"p99":911.3508582073085}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:47+00","p50":209.23550293666665,"p95":896.5684389798416,"p99":904.0733712780292}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:48+00","p50":193.35127931770833,"p95":951.2769555960755,"p99":965.730481044821}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:49+00","p50":182.1561385940171,"p95":985.5001195023889,"p99":995.5946205675023}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:50+00","p50":177.65264614583336,"p95":976.3168087130532,"p99":982.5726902734426}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:51+00","p50":160.18095674015424,"p95":976.5649679583014,"p99":986.0586683236081}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:52+00","p50":204.7120302018633,"p95":1062.7176580072903,"p99":1105.3566407063154}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:53+00","p50":182.12277178124998,"p95":1085.5990803267287,"p99":1101.585922747551}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:54+00","p50":161.88833861458332,"p95":1016.6041104890302,"p99":1023.1347571974816}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:55+00","p50":224.1995122032,"p95":972.4807652942657,"p99":982.254077234656}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:56+00","p50":112.72764324159999,"p95":1021.1596308677192,"p99":1034.954396231489}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:57+00","p50":123.68548815509692,"p95":995.1421693875045,"p99":1011.8594779703202}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:58+00","p50":110.46435114285714,"p95":988.3930815796926,"p99":999.49619827619}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:03:59+00","p50":111.23262519696395,"p95":875.4643364413648,"p99":910.5437768644919}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:00+00","p50":116.76834400986395,"p95":884.3310604246352,"p99":891.1197488559299}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:01+00","p50":150.38940723346943,"p95":913.3484174377658,"p99":947.3352900740754}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:02+00","p50":164.41859290449472,"p95":956.7746802103353,"p99":965.6958495107867}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:03+00","p50":148.53115804366374,"p95":916.0274975615379,"p99":924.9837716808776}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:04+00","p50":111.95279779815127,"p95":1014.1248942003959,"p99":1025.8983820682568}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:05+00","p50":119.31108032342658,"p95":967.5325576260001,"p99":1002.6784837033617}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:06+00","p50":172.25874897244447,"p95":859.6120768057773,"p99":883.2215528138079}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:07+00","p50":137.94197588918658,"p95":930.7868428188794,"p99":941.9507963726143}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:08+00","p50":133.4010138448,"p95":934.9220287737123,"p99":940.6645350700142}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:09+00","p50":103.11510803512573,"p95":941.2534887842132,"p99":947.9488683383596}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:10+00","p50":91.96103240640001,"p95":898.1018552500914,"p99":904.6339703836537}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:11+00","p50":180.3135502,"p95":888.0364606479837,"p99":906.5303159146384}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:12+00","p50":161.44655935204082,"p95":951.2117182863581,"p99":964.4816501748162}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:13+00","p50":139.061802457793,"p95":865.5405732792343,"p99":939.4155952435468}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:14+00","p50":164.90762919200003,"p95":845.3745126467409,"p99":876.6690966425689}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:15+00","p50":115.38199389863946,"p95":904.9112794132648,"p99":914.3180408120573}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:16+00","p50":166.5875607808,"p95":806.3492211791872,"p99":835.3906772872444}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:17+00","p50":131.87528018611113,"p95":841.3200211557304,"p99":848.4375712414321}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:18+00","p50":219.3531823555555,"p95":873.541558836053,"p99":885.0876886073328}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:19+00","p50":160.1175126671775,"p95":866.9071950594604,"p99":886.1454005992489}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:20+00","p50":162.35637656925286,"p95":804.5556653134045,"p99":823.5494711727778}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:21+00","p50":155.04343263839752,"p95":803.8871619956741,"p99":821.3600367462096}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:22+00","p50":152.90894784025045,"p95":903.9134915834957,"p99":929.4413325427205}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:23+00","p50":170.4570016186557,"p95":818.8339662388455,"p99":873.9409254911559}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:24+00","p50":172.68020002083333,"p95":845.3830045688368,"p99":854.8383360386773}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:25+00","p50":152.23743731666661,"p95":857.904825760919,"p99":865.2502107066938}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:26+00","p50":183.20789638285711,"p95":940.3095347901968,"p99":948.898699955673}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:27+00","p50":192.72122318130081,"p95":925.9054584633344,"p99":934.848886929167}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:28+00","p50":77.46190369010415,"p95":847.590966023124,"p99":866.0596553487301}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:29+00","p50":274.661046925,"p95":828.2035798139921,"p99":846.9866758195132}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:30+00","p50":123.40815575860995,"p95":820.2020626034622,"p99":825.9075731050762}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:31+00","p50":183.96436892564103,"p95":815.6051421804187,"p99":823.6086093492614}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:32+00","p50":140.67197896264366,"p95":851.6226462746481,"p99":871.9888784199641}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:33+00","p50":114.08151120138889,"p95":823.5685527860032,"p99":833.328630097261}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:34+00","p50":104.32196763059312,"p95":819.8494468170704,"p99":827.5609066651635}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:35+00","p50":225.72746272916666,"p95":811.8394493557644,"p99":818.4716249476314}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:36+00","p50":179.83959070896162,"p95":817.0405515842854,"p99":823.3596686035834}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:37+00","p50":191.7147423132154,"p95":801.2187701105119,"p99":811.0423971277019}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:38+00","p50":149.00381205635964,"p95":872.817377843378,"p99":895.3343173069369}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:39+00","p50":166.11590631770835,"p95":875.4359382710836,"p99":891.9732490807093}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:40+00","p50":179.47959301144638,"p95":835.527584006252,"p99":850.5662877712923}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:41+00","p50":184.00181869506173,"p95":825.2261374310511,"p99":855.9909098241889}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:42+00","p50":182.0279213,"p95":888.0606953566664,"p99":913.5113460817098}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:43+00","p50":129.6805893454861,"p95":876.4754347001206,"p99":895.0417583974049}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:44+00","p50":190.72440746080002,"p95":819.9556809389252,"p99":833.6203002224335}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:45+00","p50":193.3540100860215,"p95":845.5025058028335,"p99":864.7419217468744}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:46+00","p50":154.1853162814815,"p95":832.7288888664054,"p99":844.6462435467676}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:47+00","p50":191.87694016618286,"p95":841.5191597021891,"p99":849.6675349502274}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:48+00","p50":172.0618102832579,"p95":872.6316472328068,"p99":881.6051022509204}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:49+00","p50":118.64551917219512,"p95":891.9253846479108,"p99":897.3085948922086}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:50+00","p50":140.9357480478668,"p95":844.6269497257568,"p99":895.4212799655477}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:51+00","p50":180.96675250066667,"p95":915.436884182983,"p99":926.145337921917}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:52+00","p50":182.60440423882326,"p95":862.6707706808812,"p99":873.4398874402524}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:53+00","p50":94.19840458316499,"p95":879.1465363652595,"p99":885.9903438584475}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:54+00","p50":281.421298252551,"p95":896.0380751278549,"p99":908.0285198328431}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:55+00","p50":151.0533884284004,"p95":834.040434524423,"p99":888.8463721520937}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:56+00","p50":186.31885230581634,"p95":993.4565772833157,"p99":1006.485988768995}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:57+00","p50":129.9460913107823,"p95":972.6340722274103,"p99":981.0739459591472}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:58+00","p50":200.48370275,"p95":933.3977241433656,"p99":956.022705667761}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:04:59+00","p50":197.4871758656,"p95":843.2186144237969,"p99":869.5832064366061}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:00+00","p50":107.3915481941924,"p95":849.1166180816022,"p99":860.7183430700242}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:01+00","p50":142.8716569285714,"p95":877.7212447899188,"p99":889.0832020152066}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:02+00","p50":213.51936688499026,"p95":884.8529600235786,"p99":896.9564586420423}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:03+00","p50":122.0763227465278,"p95":839.5891034400511,"p99":857.3971063863174}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:04+00","p50":129.50813146363637,"p95":838.45111324854,"p99":847.3936020367389}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:05+00","p50":152.71266851250485,"p95":819.1587348950327,"p99":827.5261625258656}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:06+00","p50":192.3731175525792,"p95":836.3770732209238,"p99":853.0784123982688}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:07+00","p50":141.53706735999998,"p95":863.6779031890835,"p99":877.7524627511517}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:08+00","p50":150.29622478867438,"p95":883.448551324836,"p99":895.3153187908673}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:09+00","p50":166.97223737220844,"p95":888.8596368541516,"p99":907.4631847607508}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:10+00","p50":168.14903360416665,"p95":836.7020059641211,"p99":842.6018494223188}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:11+00","p50":161.44474604624423,"p95":843.6477612890842,"p99":850.0584010756967}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:12+00","p50":134.8340068731973,"p95":883.7097845518987,"p99":899.4763029331075}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:13+00","p50":146.48330126857002,"p95":880.5503852341052,"p99":893.7131090192627}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:14+00","p50":148.97041208688864,"p95":882.0733252448327,"p99":892.6639732366282}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:15+00","p50":188.74677950141557,"p95":847.8840677035021,"p99":860.4342265109735}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:16+00","p50":168.74620900609753,"p95":853.2454907497357,"p99":863.4284157735605}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:17+00","p50":146.11253602256943,"p95":812.5305088152713,"p99":833.2500078782138}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:18+00","p50":132.00140046006945,"p95":859.7655308567181,"p99":869.7588902066301}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:19+00","p50":138.80293983333334,"p95":907.526780548504,"p99":934.1281860940505}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:20+00","p50":139.87628454570157,"p95":891.048569500218,"p99":921.1187058316094}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:21+00","p50":140.1979240633333,"p95":840.97996561889,"p99":847.8145740474141}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:22+00","p50":198.90589620095122,"p95":909.8853588031607,"p99":924.4919901618598}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:23+00","p50":184.77680234693875,"p95":820.833960539598,"p99":860.804364216038}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:24+00","p50":170.11590695572917,"p95":834.0140575928192,"p99":841.3142495561751}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:25+00","p50":152.06705004837804,"p95":867.6596708856902,"p99":888.849981078496}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:26+00","p50":163.73232836419754,"p95":955.6284881550899,"p99":969.4899485513339}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:27+00","p50":160.49244882089923,"p95":946.1397630557085,"p99":961.329233185197}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:28+00","p50":171.36047114044297,"p95":863.3397951197086,"p99":873.3484213171623}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:29+00","p50":174.98410024245575,"p95":817.7546987438155,"p99":831.8507173073291}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:30+00","p50":125.55656867417581,"p95":847.9341209712131,"p99":856.4512946739384}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:31+00","p50":177.6434368971088,"p95":802.5671538721213,"p99":817.0953374670335}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:32+00","p50":189.70806530242587,"p95":901.729172989922,"p99":916.5937888871649}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:33+00","p50":150.4833363888889,"p95":813.0173956254675,"p99":889.3513372021614}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:34+00","p50":122.04424042183622,"p95":802.2271582787143,"p99":807.7741341994122}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:35+00","p50":341.9106411937415,"p95":770.9941586335539,"p99":783.3869970573517}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:36+00","p50":335.82002696480004,"p95":746.5177738208878,"p99":763.8672478751107}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:37+00","p50":458.1015817265625,"p95":599.1254437775463,"p99":607.1927945710612}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:38+00","p50":258.9196548805555,"p95":661.6916260039709,"p99":678.5705793731781}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:39+00","p50":328.23025735171694,"p95":674.0927366052783,"p99":683.0621881832118}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:40+00","p50":179.2275500130612,"p95":636.5371593075872,"p99":645.2843875661497}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:41+00","p50":185.6950034771072,"p95":590.7294421625011,"p99":605.0131187773453}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:42+00","p50":322.346815241432,"p95":631.5183783101332,"p99":641.6835554316442}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:43+00","p50":135.3548357485366,"p95":632.1075382865163,"p99":640.192861615467}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:44+00","p50":260.0389664470868,"p95":816.6905537319011,"p99":831.0769471117806}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:45+00","p50":283.5578886840001,"p95":804.5768434482991,"p99":826.9554999763552}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:46+00","p50":196.2779368576389,"p95":806.6067533162607,"p99":830.4651415380974}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:47+00","p50":104.87109549074076,"p95":833.9636702858843,"p99":838.6670371823305}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:48+00","p50":173.22605064775854,"p95":832.618332443579,"p99":839.9133747691769}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:49+00","p50":214.73754851921186,"p95":828.9682704645428,"p99":851.2501085848703}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:50+00","p50":150.80225555224456,"p95":808.3786789971215,"p99":833.0671200588665}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:51+00","p50":173.908721226087,"p95":865.9056723616602,"p99":876.5186923963797}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:52+00","p50":185.36504035565216,"p95":891.9269155197624,"p99":902.5161164552254}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:53+00","p50":196.33771104111113,"p95":880.3202213392066,"p99":909.0578918998153}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:54+00","p50":189.26141954399998,"p95":870.5828449015429,"p99":886.4967480701769}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:55+00","p50":218.5570775390071,"p95":808.5295165447588,"p99":840.9662854646947}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:56+00","p50":275.7165370346021,"p95":942.1340431241915,"p99":954.3732600148936}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:57+00","p50":198.61435225000002,"p95":937.6633734625623,"p99":952.6139766026736}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:58+00","p50":157.76123945833334,"p95":919.2899651256057,"p99":925.5849828712401}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:05:59+00","p50":182.95862393940882,"p95":870.8557939714607,"p99":878.9871486913752}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:00+00","p50":141.32525285253908,"p95":851.0409201405515,"p99":868.09872412629}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:01+00","p50":107.57274975520833,"p95":839.7470789091428,"p99":849.0125394489106}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:02+00","p50":122.00681259090909,"p95":829.1260181997429,"p99":835.0841027198225}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:03+00","p50":152.47291779121244,"p95":899.4875990820124,"p99":916.7468151209271}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:04+00","p50":142.09549878311967,"p95":843.2210050285286,"p99":849.6521120060152}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:05+00","p50":159.64177131250003,"p95":821.7211610919572,"p99":831.1726534922168}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:06+00","p50":117.46127604129033,"p95":845.3085856225742,"p99":866.8034888236348}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:07+00","p50":153.8493180954861,"p95":810.5903981406843,"p99":829.4838218790253}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:08+00","p50":151.44196150694444,"p95":878.3598685598062,"p99":895.2653950316252}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:09+00","p50":80.46332879310344,"p95":891.5284475983667,"p99":900.214580177584}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:10+00","p50":99.27196940451387,"p95":878.2504103501036,"p99":898.8800232576583}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:11+00","p50":145.30588924193552,"p95":834.7488084411623,"p99":843.6140024256306}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:12+00","p50":141.39370970000007,"p95":872.358400453881,"p99":881.7194287904596}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:13+00","p50":135.05293786801428,"p95":877.5958177076849,"p99":900.218742982496}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:14+00","p50":155.99773729179273,"p95":817.9543655553961,"p99":826.7090539721554}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:15+00","p50":161.41396590625004,"p95":828.9572689682824,"p99":839.4417460503992}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:16+00","p50":181.72705512830183,"p95":829.1701377083015,"p99":843.8705648227862}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:17+00","p50":156.15259033152176,"p95":810.009023174813,"p99":821.8229018733906}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:18+00","p50":203.74004233962265,"p95":902.405142612815,"p99":911.6710367658736}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:19+00","p50":222.92661695999996,"p95":895.4473994186825,"p99":909.9098163684412}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:20+00","p50":155.4374491152,"p95":890.9781440088942,"p99":905.5089575465074}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:21+00","p50":151.1133701986825,"p95":922.0058330167215,"p99":932.9621112307574}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:22+00","p50":180.18373729846942,"p95":924.4423993199435,"p99":940.2614376946437}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:23+00","p50":192.94474238287756,"p95":866.1837719217657,"p99":889.0176447175877}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:24+00","p50":105.6382134745578,"p95":870.434761376513,"p99":893.5471376983903}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:25+00","p50":107.89911800347221,"p95":838.1235074807208,"p99":857.4694506042819}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:26+00","p50":138.01928438265307,"p95":906.229693226538,"p99":916.6829046084447}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:27+00","p50":124.35965741340138,"p95":890.5532258417895,"p99":909.9889361694977}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:28+00","p50":100.52768965348594,"p95":851.0604469979017,"p99":862.7802309410746}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:29+00","p50":67.22237695805556,"p95":837.9830239860305,"p99":853.2633361830394}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:30+00","p50":126.99094629687502,"p95":832.0627577004603,"p99":847.1064091260996}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:31+00","p50":133.94317067215363,"p95":828.4506078998609,"p99":839.115035907801}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:32+00","p50":160.3778032208,"p95":835.7837551165144,"p99":853.8817000392868}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:33+00","p50":153.83455269333334,"p95":872.8117593149381,"p99":892.7757373123518}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:34+00","p50":145.7967649952031,"p95":859.5958394422315,"p99":868.1680034805274}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:35+00","p50":121.27946326736111,"p95":844.610243059293,"p99":855.2671421693121}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:36+00","p50":169.7355412561794,"p95":830.7717871870956,"p99":844.4441607912775}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:37+00","p50":106.35374383641974,"p95":823.2788269930013,"p99":844.2322007787341}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:38+00","p50":136.26873930492425,"p95":835.1196865300786,"p99":845.6271297810973}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:39+00","p50":142.209114784,"p95":839.3400342879867,"p99":851.1243195119146}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:40+00","p50":184.15720095679998,"p95":837.4893090305309,"p99":860.7417074193781}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:41+00","p50":179.9909550214534,"p95":833.3675549463413,"p99":840.4379954888823}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:42+00","p50":175.7529329760666,"p95":836.0509372981903,"p99":845.017752447741}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:43+00","p50":144.44677240985575,"p95":854.4870822800311,"p99":874.6723709965837}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:44+00","p50":142.1307243125,"p95":858.2855717562261,"p99":870.9355916620007}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:45+00","p50":112.85989301791855,"p95":843.362781874958,"p99":856.051467242243}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:46+00","p50":160.80131303376808,"p95":848.8830472922502,"p99":869.5373092129303}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:47+00","p50":169.07398805567118,"p95":844.0324042362133,"p99":855.036505647307}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:48+00","p50":188.2702191377551,"p95":828.0999145178714,"p99":856.5236696306731}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:49+00","p50":176.61449457777778,"p95":835.3213487836317,"p99":874.6627016886429}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:50+00","p50":125.8550488489026,"p95":859.5257986372243,"p99":911.1747673327988}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:51+00","p50":157.71621254861114,"p95":868.4787277852541,"p99":890.2335295346035}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:52+00","p50":126.2046562109375,"p95":912.8839405155943,"p99":924.473734679956}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:53+00","p50":229.79014606122448,"p95":891.5954764658277,"p99":902.8474215219877}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:54+00","p50":130.021756389378,"p95":907.408449443192,"p99":916.795416582843}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:55+00","p50":154.78921833211854,"p95":920.9965613112541,"p99":958.0468899362029}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:56+00","p50":150.13482995238095,"p95":1026.3279199498188,"p99":1037.7145626136964}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:57+00","p50":170.96890766758506,"p95":958.1773285307609,"p99":985.4937783821645}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:58+00","p50":122.08066436284723,"p95":959.0200199835149,"p99":970.3053189972574}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:06:59+00","p50":171.84472884558664,"p95":920.1171047884113,"p99":931.4256133445858}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:00+00","p50":174.99185420562398,"p95":924.9397580594557,"p99":942.7213833548636}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:01+00","p50":177.9066445835668,"p95":817.138424450561,"p99":832.061204869453}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:02+00","p50":119.43115156527207,"p95":875.5225984204892,"p99":895.8237609765812}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:03+00","p50":187.4149137954545,"p95":872.3575949473724,"p99":890.9880670752362}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:04+00","p50":183.8981345317378,"p95":830.4770125488456,"p99":847.2705703136091}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:05+00","p50":137.95978750086803,"p95":821.5833477387006,"p99":829.7543390264059}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:06+00","p50":97.48673016912879,"p95":870.7752721492491,"p99":880.3384775713034}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:07+00","p50":175.263430844,"p95":865.8098528732603,"p99":891.995503250428}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:08+00","p50":142.67212664095706,"p95":894.9192960395326,"p99":903.2366994402992}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:09+00","p50":125.97986903809525,"p95":894.5155005873121,"p99":906.0029393268068}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:10+00","p50":152.54004705555556,"p95":875.7114135471285,"p99":884.9548846478233}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:11+00","p50":157.7046710206335,"p95":877.7040892602992,"p99":900.7094262792905}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:12+00","p50":164.39097716319444,"p95":908.6744261054542,"p99":915.6355940711767}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:13+00","p50":284.7095186460606,"p95":898.3151636160475,"p99":917.0375236029803}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:14+00","p50":213.03792012573925,"p95":749.1985433668476,"p99":763.766683781034}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:15+00","p50":205.98285457999998,"p95":785.7817052120582,"p99":799.5526294006397}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:16+00","p50":211.7603389921875,"p95":793.0057471682643,"p99":816.6120188770315}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:17+00","p50":174.18426664211162,"p95":754.1159916667813,"p99":792.6925114743254}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:18+00","p50":179.2047711603128,"p95":872.838082858378,"p99":877.4244200486241}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:19+00","p50":171.46971292222221,"p95":845.9903450696264,"p99":858.9792886168863}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:20+00","p50":139.22590992805038,"p95":808.8359739343567,"p99":825.248686739155}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:21+00","p50":190.35605077098154,"p95":819.7416415341316,"p99":826.5747291442092}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:22+00","p50":215.35823348072395,"p95":820.5884039275185,"p99":833.0751775190319}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:23+00","p50":215.5791993912,"p95":827.9944940936318,"p99":843.8151916440856}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:24+00","p50":173.66170294979395,"p95":865.8284303524565,"p99":878.8791011460606}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:25+00","p50":223.06623384320002,"p95":886.1519178884164,"p99":904.6845428379652}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:26+00","p50":164.09400109743592,"p95":919.8721243591662,"p99":934.1257135227352}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:27+00","p50":187.35874202218474,"p95":929.6443846587513,"p99":947.0928570580205}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:28+00","p50":165.516768375,"p95":833.027553444034,"p99":855.2181947699839}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:29+00","p50":136.31270867361113,"p95":815.0575312822376,"p99":830.64018129213}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:30+00","p50":176.938902894065,"p95":820.6060925855465,"p99":834.0696043369943}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:31+00","p50":177.06076098557693,"p95":811.3342447303145,"p99":819.2631865897106}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:32+00","p50":184.30300594408607,"p95":790.2555388643713,"p99":796.6814616933741}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:33+00","p50":204.49080381052633,"p95":825.5172701699977,"p99":843.3632020569371}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:34+00","p50":153.77613428358976,"p95":751.1909940446617,"p99":789.248395382599}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:35+00","p50":114.54377559579771,"p95":825.9143976222314,"p99":837.8769394790485}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:36+00","p50":247.47375248535158,"p95":789.876427124331,"p99":798.3889681670313}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:37+00","p50":172.73232440277778,"p95":806.6449977119463,"p99":816.1910754363853}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:38+00","p50":325.398099,"p95":792.2691494821295,"p99":805.9173635179769}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:39+00","p50":93.70890329685312,"p95":789.0943503013455,"p99":800.2168825172714}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:40+00","p50":204.56434037870372,"p95":834.1482735117671,"p99":847.8106218886784}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:41+00","p50":130.39035763735393,"p95":729.0027139269567,"p99":757.3720691801841}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:42+00","p50":126.93971052441404,"p95":822.2740745356435,"p99":828.1600827047412}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:43+00","p50":186.31530989013675,"p95":805.1994792905608,"p99":815.4766477084686}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:44+00","p50":131.958055109375,"p95":782.3304271742662,"p99":795.3120056817778}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:45+00","p50":283.69244049084244,"p95":779.4847170462791,"p99":788.9455284836392}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:46+00","p50":144.8195580128,"p95":730.2671791043707,"p99":742.1600403851279}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:47+00","p50":233.8856215599374,"p95":786.3020844201052,"p99":810.56854603422}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:48+00","p50":137.61986164175144,"p95":730.2251913916592,"p99":751.561282982829}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:49+00","p50":115.63770049652777,"p95":834.4170401223254,"p99":858.4096711135629}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:50+00","p50":277.37367407291663,"p95":807.6329182179355,"p99":837.9062920357043}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:51+00","p50":287.6548706631515,"p95":801.0087704469422,"p99":812.8283146900594}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:52+00","p50":196.6059070642948,"p95":844.4221826444647,"p99":852.6863916745668}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:53+00","p50":108.2624109032258,"p95":837.2047611834575,"p99":847.0441391934694}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:54+00","p50":254.35990261530503,"p95":828.9909491223203,"p99":844.2431132804402}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:55+00","p50":243.96176963833332,"p95":775.8911544442304,"p99":793.3971573531561}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:56+00","p50":291.059954,"p95":863.369279167034,"p99":875.6499854588768}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:57+00","p50":118.48303570731709,"p95":880.989835708958,"p99":893.8563512992562}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:58+00","p50":134.69762048,"p95":908.3543602514915,"p99":935.2729659314009}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:07:59+00","p50":159.51573318939396,"p95":774.7984025494883,"p99":787.8519464922028}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:00+00","p50":132.5374775147569,"p95":811.6646636639717,"p99":819.2798330214346}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:01+00","p50":154.83727492154244,"p95":829.8926885812377,"p99":844.404753005495}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:02+00","p50":137.09910627083332,"p95":826.1869154720674,"p99":832.2116044636093}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:03+00","p50":119.82633751947395,"p95":798.7530535757585,"p99":805.1853546832689}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:04+00","p50":179.58680110833336,"p95":822.2517363193624,"p99":840.3355586332464}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:05+00","p50":140.49727524450677,"p95":793.3923456879351,"p99":845.6200748597755}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:06+00","p50":92.239490921875,"p95":801.9111034237285,"p99":816.6921848320376}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:07+00","p50":164.86558792285155,"p95":790.8018642219823,"p99":796.2994825770851}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:08+00","p50":147.91428722525347,"p95":836.6661536879933,"p99":859.951300117742}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:09+00","p50":96.8338336656,"p95":835.4646435186899,"p99":841.455774904582}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:10+00","p50":141.39275824471997,"p95":790.0044652435289,"p99":798.9681150866879}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:11+00","p50":189.0163824326543,"p95":808.4377594585421,"p99":820.2099867701414}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:12+00","p50":148.95533943434344,"p95":745.4517380558287,"p99":753.5271859490356}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:13+00","p50":200.22043302705512,"p95":754.8854817906715,"p99":765.9465170839632}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:14+00","p50":179.3480331592663,"p95":755.2599911140188,"p99":768.6816938221807}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:15+00","p50":146.79876352559998,"p95":738.0021462563208,"p99":765.7426607790699}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:16+00","p50":148.22170313020834,"p95":750.7971054477822,"p99":758.4167452952031}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:17+00","p50":158.82188277250674,"p95":738.0247322961284,"p99":771.0450949015465}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:18+00","p50":141.35479228701737,"p95":814.7980724699559,"p99":836.234756732638}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:19+00","p50":177.77512096602564,"p95":782.9126407028248,"p99":790.7081046791676}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:20+00","p50":167.55360489761907,"p95":764.3369895335101,"p99":772.4445925343172}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:21+00","p50":161.19875176549021,"p95":827.0744201001902,"p99":837.3763378016707}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:22+00","p50":108.59934394791667,"p95":853.1787799990241,"p99":863.8886851681983}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:23+00","p50":165.86117635894428,"p95":850.6935301706155,"p99":856.3128832328475}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:24+00","p50":150.6563847633333,"p95":831.1178533441479,"p99":841.4003079275906}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:25+00","p50":79.87625581913042,"p95":892.2581449218308,"p99":959.7820640087539}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:26+00","p50":352.38895382274256,"p95":1020.5813153486818,"p99":1032.9922865051162}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:27+00","p50":308.1781168768993,"p95":963.2583324062543,"p99":972.0616888514495}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:28+00","p50":306.60339943112245,"p95":880.2871106654773,"p99":889.7536685674718}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:29+00","p50":102.10667625309091,"p95":852.7468620829264,"p99":859.804272218557}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:30+00","p50":106.73244325499999,"p95":841.29974704302,"p99":852.8204710066244}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:31+00","p50":321.27973864579184,"p95":800.4802991511845,"p99":815.8964421284564}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:32+00","p50":257.2825462662722,"p95":765.2597599540073,"p99":779.4251556515004}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:33+00","p50":117.1543098289855,"p95":800.214465088744,"p99":810.7740819919127}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:34+00","p50":302.4475485432,"p95":833.9859642841504,"p99":842.6462564449073}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:35+00","p50":246.95489917567642,"p95":794.5601211796804,"p99":806.9333227371661}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:36+00","p50":173.4931907763265,"p95":747.7596851817788,"p99":770.7683577086774}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:37+00","p50":143.39463413423516,"p95":795.3996531250689,"p99":801.4254779834108}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:38+00","p50":178.8215507355442,"p95":843.3704430013532,"p99":853.103654267444}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:39+00","p50":326.7416939528735,"p95":851.0260335509452,"p99":868.0524042703017}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:40+00","p50":273.75606001879703,"p95":742.9697219073711,"p99":755.1988105288436}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:41+00","p50":163.6017847906667,"p95":761.5427756520533,"p99":776.307463291584}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:42+00","p50":188.77106168112246,"p95":880.2353590326077,"p99":916.9919280625141}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:43+00","p50":157.59157576649304,"p95":926.6714959521993,"p99":933.2029208649777}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:44+00","p50":183.21288769321268,"p95":867.5303671657156,"p99":875.4152709046724}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:45+00","p50":172.58899126060606,"p95":870.0274403956568,"p99":878.1639216817588}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:46+00","p50":136.56726848271495,"p95":887.5704041545144,"p99":908.0241472114212}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:47+00","p50":122.21778811875,"p95":883.8204975849013,"p99":901.6188352780869}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:48+00","p50":117.90681488541667,"p95":965.0791290914364,"p99":972.0437182757062}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:49+00","p50":166.133769264,"p95":956.5017124201373,"p99":968.381952799801}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:50+00","p50":156.1362717826087,"p95":913.6774881187687,"p99":923.0467272075073}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:51+00","p50":182.91901600160253,"p95":977.4117052932439,"p99":998.238896513396}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:52+00","p50":221.6446938076923,"p95":971.0413254832799,"p99":981.029716571432}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:53+00","p50":204.66321311243905,"p95":949.1028103463277,"p99":976.848530159485}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:54+00","p50":134.3446034791667,"p95":1013.3629291164572,"p99":1023.6935573351959}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:55+00","p50":176.99315628689658,"p95":873.6158796141603,"p99":909.0246995574494}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:56+00","p50":187.62995227083334,"p95":988.9558215975462,"p99":1003.8324746383641}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:57+00","p50":419.5135298645833,"p95":954.8864993630551,"p99":978.1137520724482}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:58+00","p50":360.7198704580499,"p95":1115.579169416203,"p99":1133.0257532518403}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:08:59+00","p50":132.8904335837912,"p95":1035.4947424301918,"p99":1065.8775668192766}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:00+00","p50":207.94988857,"p95":1023.6225219825213,"p99":1031.9716993169816}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:01+00","p50":92.92581579012345,"p95":986.5000283152684,"p99":998.4529732855301}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:02+00","p50":240.27886289431817,"p95":980.5087290152647,"p99":989.1441424989534}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:03+00","p50":211.86449798109638,"p95":1036.5129554345745,"p99":1055.5936941271857}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:04+00","p50":165.98759580555557,"p95":1061.2468185533182,"p99":1077.8474626889326}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:05+00","p50":202.64146762844703,"p95":970.3902405155532,"p99":999.2682586459133}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:06+00","p50":164.67191985937498,"p95":926.4004704440948,"p99":933.1060915909721}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:07+00","p50":229.37524920233196,"p95":923.3430567615446,"p99":939.6936660642805}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:08+00","p50":395.5790240530303,"p95":1001.4622257344301,"p99":1008.014763439697}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:09+00","p50":348.7548057951388,"p95":1041.0793781617176,"p99":1053.8630586854047}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:10+00","p50":330.64562607481855,"p95":910.1581524212788,"p99":981.201424566279}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:11+00","p50":122.51356805104072,"p95":874.9676435039926,"p99":934.533404202676}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:12+00","p50":284.64822443630817,"p95":1001.5030864290636,"p99":1010.147976504706}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:13+00","p50":131.90820698492738,"p95":895.5356105548518,"p99":910.2151579304515}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:14+00","p50":156.27504727343754,"p95":883.3884247818806,"p99":897.8318793304602}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:15+00","p50":213.09676487200832,"p95":821.6040067564068,"p99":839.0098718815977}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:16+00","p50":148.00018924224491,"p95":836.8263725581286,"p99":842.0758055745646}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:17+00","p50":216.97690333333333,"p95":841.4191748727916,"p99":857.1826109781853}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:18+00","p50":110.81297658007206,"p95":870.9784739412257,"p99":888.5048857968324}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:19+00","p50":232.56279648765434,"p95":932.747943678068,"p99":941.6033641988787}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:20+00","p50":174.90163273263886,"p95":853.0830003029971,"p99":891.9499810374264}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:21+00","p50":197.82891518724202,"p95":982.2083995796822,"p99":1004.6279243257801}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:22+00","p50":188.1176668139456,"p95":1047.1851475610003,"p99":1059.156943218619}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:23+00","p50":187.59935938090905,"p95":973.7805985331205,"p99":1028.0082248582726}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:24+00","p50":188.19978416351648,"p95":945.1158830352678,"p99":956.3432570486177}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:25+00","p50":194.079518125,"p95":916.7715002782151,"p99":926.6647193529532}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:26+00","p50":171.807471828125,"p95":975.3143047601363,"p99":995.7264173628159}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:27+00","p50":215.9826261779337,"p95":938.5525203764395,"p99":950.5548861658468}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:28+00","p50":202.3234710176,"p95":972.5082403886812,"p99":1034.2565435258214}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:29+00","p50":168.05932265666664,"p95":835.5191745670895,"p99":849.1273971236337}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:30+00","p50":124.981233590379,"p95":861.3794505190368,"p99":872.7274967310702}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:31+00","p50":162.0610556242173,"p95":869.1633688181888,"p99":874.4879434530678}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:32+00","p50":107.18443183333333,"p95":873.2807993804741,"p99":880.4240872087767}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:33+00","p50":387.9451670740741,"p95":876.2834293559375,"p99":885.7712936414973}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:34+00","p50":317.15956802612334,"p95":824.027211231282,"p99":828.7586825205548}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:35+00","p50":296.67659196799997,"p95":825.2404098583359,"p99":834.4684458496907}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:36+00","p50":202.24116183388887,"p95":849.3757727475789,"p99":859.451061439056}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:37+00","p50":125.38386835084175,"p95":846.6801238458123,"p99":856.7119313559897}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:38+00","p50":111.24819899652778,"p95":910.9217984351507,"p99":923.2873402298192}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:39+00","p50":193.58472444984045,"p95":869.2287711750304,"p99":883.6450729343849}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:40+00","p50":179.11243579631338,"p95":808.1279261349066,"p99":819.8973995055549}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:41+00","p50":163.45019937499998,"p95":818.7858223591192,"p99":832.2873394601266}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:42+00","p50":154.14760645054943,"p95":867.0242086263185,"p99":892.8478135799802}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:43+00","p50":187.43728446440971,"p95":850.2504187882936,"p99":860.0196165170319}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:44+00","p50":220.9440701808086,"p95":915.8030228126722,"p99":932.2156839993386}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:45+00","p50":160.03511016719577,"p95":922.8189448520278,"p99":936.5241384139482}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:46+00","p50":120.34242616646848,"p95":915.8226468851482,"p99":930.0328075187457}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:47+00","p50":113.60138448415657,"p95":874.2721442081835,"p99":897.4510779067537}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:48+00","p50":191.86672659808485,"p95":930.8260887541295,"p99":943.0054520962824}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:49+00","p50":189.15229458263968,"p95":938.4633609682277,"p99":963.0073211340334}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:50+00","p50":177.031243636,"p95":840.6384409823049,"p99":874.1999570827032}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:51+00","p50":170.96667820714285,"p95":850.1573622938108,"p99":861.2461128623148}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:52+00","p50":191.738515226601,"p95":880.5103567704699,"p99":890.4381478972114}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:53+00","p50":156.63287782499998,"p95":864.8865806230847,"p99":885.7164055835507}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:54+00","p50":164.36694672115382,"p95":854.3391238032689,"p99":862.2920887671402}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:55+00","p50":177.2212240911111,"p95":844.513401494492,"p99":870.6330323383246}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:56+00","p50":214.61505149362247,"p95":882.1149001267972,"p99":904.7307901644227}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:57+00","p50":184.67699114482758,"p95":904.2405884345732,"p99":913.2518789987126}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:58+00","p50":173.76448632291667,"p95":896.0261988255711,"p99":912.1697403997156}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:09:59+00","p50":197.31992795074913,"p95":875.1500356517146,"p99":895.5424505337578}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:00+00","p50":163.56090163020409,"p95":765.3218828804653,"p99":786.0973245100863}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:01+00","p50":94.07016503560725,"p95":853.3220445096204,"p99":873.6185410867232}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:02+00","p50":197.7397381032356,"p95":868.9970602267358,"p99":877.9719478223939}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:03+00","p50":177.25506081666668,"p95":847.120467312767,"p99":856.0560337740568}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:04+00","p50":137.13245549333337,"p95":852.197606403841,"p99":858.4422876319645}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:05+00","p50":165.99926910812428,"p95":844.938458156285,"p99":853.1762635590443}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:06+00","p50":182.01431349739855,"p95":853.1545409248797,"p99":862.6554423544384}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:07+00","p50":176.45414991883456,"p95":840.9445652114963,"p99":855.4048047598965}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:08+00","p50":171.23971045061728,"p95":963.3841835088584,"p99":975.3357453895511}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:09+00","p50":153.9903397669775,"p95":977.3760339944135,"p99":988.4870111269}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:10+00","p50":200.99544390582375,"p95":872.5305877935219,"p99":891.6282298050946}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:11+00","p50":162.8197212117347,"p95":888.9681668182017,"p99":912.1772142584937}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:12+00","p50":172.76009902386554,"p95":936.0413173568354,"p99":949.8255237300045}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:13+00","p50":187.3331094294862,"p95":888.9521226836594,"p99":902.411018103393}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:14+00","p50":188.34354261447808,"p95":832.2854849634587,"p99":847.5369146788208}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:15+00","p50":149.1403953283038,"p95":840.5614472034306,"p99":848.2252575414409}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:16+00","p50":160.58630235416666,"p95":835.2589295476068,"p99":844.1790329531406}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:17+00","p50":169.92507091025638,"p95":815.966821391961,"p99":833.3199464153097}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:18+00","p50":124.1631464375,"p95":853.0767238692497,"p99":863.3597467488605}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:19+00","p50":146.0937446314611,"p95":862.9221623470866,"p99":872.2214198684363}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:20+00","p50":121.46128107040818,"p95":840.6713698088469,"p99":851.1471098683835}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:21+00","p50":266.23263089298456,"p95":847.7736574589732,"p99":865.907572981422}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:22+00","p50":167.77067444693878,"p95":875.3627235459031,"p99":884.7950117067304}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:23+00","p50":142.4297070792,"p95":888.4226343158474,"p99":903.1257126532295}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:24+00","p50":328.6672529875,"p95":934.892167494363,"p99":948.2912934344624}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:25+00","p50":214.26317070577784,"p95":878.7871477416882,"p99":894.3978168366898}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:26+00","p50":159.68408152153847,"p95":971.0352644238192,"p99":977.6419470007986}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:27+00","p50":150.1629603938992,"p95":977.0155990187691,"p99":991.7435979842298}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:28+00","p50":136.6727594367816,"p95":876.0869645325334,"p99":891.8145658595572}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:29+00","p50":180.8011044459111,"p95":854.9526814707937,"p99":863.1758217384589}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:30+00","p50":176.6464631146612,"p95":873.5461433089163,"p99":883.1445556609705}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:31+00","p50":224.64859416640004,"p95":798.1204278359847,"p99":822.2126508985306}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:32+00","p50":193.13077186666663,"p95":701.3637078919089,"p99":714.2025665074731}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:33+00","p50":99.1200232377267,"p95":799.9277325783372,"p99":814.9225848540075}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:34+00","p50":307.82025886924527,"p95":822.9180081845253,"p99":828.6089869373529}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:35+00","p50":209.54083736000004,"p95":828.1798231364445,"p99":839.661051257603}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:36+00","p50":186.44532877073624,"p95":860.1641673910867,"p99":878.0229933549709}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:37+00","p50":181.60759278125,"p95":901.3681442106541,"p99":909.4838294164404}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:38+00","p50":129.72080746400002,"p95":909.6799378096706,"p99":926.9622529268079}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:39+00","p50":194.6133341735254,"p95":973.9936750197922,"p99":985.6948396596385}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:40+00","p50":152.5924657204861,"p95":844.1275166277625,"p99":908.7581246712443}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:41+00","p50":161.24503925277773,"p95":784.8714894243392,"p99":802.6239265977845}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:42+00","p50":138.0946943998457,"p95":836.134645481657,"p99":860.4909837626376}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:43+00","p50":148.89154697048613,"p95":863.067582648499,"p99":887.4380642228174}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:44+00","p50":107.01574622754579,"p95":831.6326211966106,"p99":836.7883841018688}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:45+00","p50":243.8516694734651,"p95":841.0826528425268,"p99":862.3088225565806}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:46+00","p50":217.45439516,"p95":843.1506454871429,"p99":856.7169393826788}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:47+00","p50":191.13240648762132,"p95":837.9980486626528,"p99":854.5630091880058}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:48+00","p50":191.6013597503326,"p95":849.9791348857685,"p99":857.5287814026972}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:49+00","p50":207.03783684102024,"p95":881.3869508335943,"p99":894.5235042109763}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:50+00","p50":125.93004647409806,"p95":847.3091082330133,"p99":867.5659536457434}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:51+00","p50":242.989556555795,"p95":844.9876300143916,"p99":859.2599289070151}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:52+00","p50":193.36248114715218,"p95":876.4414313411031,"p99":881.2559201688811}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:53+00","p50":178.1297149323308,"p95":869.3623821178912,"p99":888.6297849649841}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:54+00","p50":190.21099331902585,"p95":872.3728924329929,"p99":880.1174727892901}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:55+00","p50":170.66481297993903,"p95":822.4447861818904,"p99":880.9187764708138}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:56+00","p50":147.18192818439027,"p95":1024.4772189818966,"p99":1040.1213357407912}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:57+00","p50":99.31671955519998,"p95":971.2676735418329,"p99":1006.937375249338}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:58+00","p50":101.63772614108844,"p95":921.4866354397145,"p99":937.2991035375838}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:10:59+00","p50":166.230971306288,"p95":872.7449632003343,"p99":882.1211070864763}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:00+00","p50":171.89506431269845,"p95":859.7998689238482,"p99":875.2822884428181}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:01+00","p50":151.64859563789116,"p95":798.9371321533121,"p99":816.5684739618705}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:02+00","p50":152.45507022592153,"p95":873.3668439535194,"p99":885.3945788212352}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:03+00","p50":116.44094672136055,"p95":904.3510756070987,"p99":922.3467428529924}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:04+00","p50":135.23882057279997,"p95":880.3051326311489,"p99":890.767191570479}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:05+00","p50":135.50346683333333,"p95":870.1438959715019,"p99":879.1564858108558}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:06+00","p50":110.00825342739019,"p95":946.606922975158,"p99":979.6823366872615}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:07+00","p50":113.06676714625445,"p95":970.7575281248357,"p99":978.8567681204873}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:08+00","p50":115.75154096145303,"p95":970.1982304204655,"p99":993.4021494786172}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:09+00","p50":154.88526217968752,"p95":996.0948834897014,"p99":1002.3160169579613}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:10+00","p50":154.2971809617225,"p95":983.7316551865226,"p99":1000.9446543327615}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:11+00","p50":160.21968615364582,"p95":960.3200601401472,"p99":970.1679501772646}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:12+00","p50":184.7616652830688,"p95":1054.7665742701777,"p99":1060.5565761907292}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:13+00","p50":211.61125352958578,"p95":988.347312078945,"p99":1040.6930893614922}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:14+00","p50":153.33471510000882,"p95":941.2694182528642,"p99":949.9365416841824}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:15+00","p50":153.1774247558476,"p95":894.4181080184442,"p99":909.4589553663452}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:16+00","p50":119.5056003482143,"p95":917.7142359122331,"p99":925.3398293620251}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:17+00","p50":145.28837761522635,"p95":895.3045610517221,"p99":927.5254465965048}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:18+00","p50":192.62708938886564,"p95":1064.76435955482,"p99":1097.319677465073}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:19+00","p50":272.90998545857985,"p95":1071.4004026849277,"p99":1090.1369392661159}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:20+00","p50":135.06642139583334,"p95":932.0835913073817,"p99":943.5523403880779}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:21+00","p50":151.34831890844723,"p95":952.3497728006504,"p99":959.4193800218912}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:22+00","p50":156.90832212499996,"p95":973.0640328306124,"p99":983.5040944868518}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:23+00","p50":168.71239393707484,"p95":943.3584196855387,"p99":967.7196470735879}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:24+00","p50":174.65328041645407,"p95":932.0815484663663,"p99":950.2783714826015}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:25+00","p50":150.31283015410145,"p95":932.5670135256786,"p99":938.4557964059144}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:26+00","p50":175.5939092792683,"p95":959.5806446925089,"p99":969.2619396943062}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:27+00","p50":238.42462058874463,"p95":892.1922514098281,"p99":916.9030247294769}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:28+00","p50":176.22756258817734,"p95":872.4350747465959,"p99":880.7247694181568}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:29+00","p50":175.67740075199998,"p95":901.2176802123216,"p99":908.3727602749134}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:30+00","p50":220.2904310486111,"p95":905.8261410479981,"p99":921.8519416753666}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:31+00","p50":234.34014240903687,"p95":787.7232057231653,"p99":793.1561143971799}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:32+00","p50":198.3342561206597,"p95":761.0007040620085,"p99":772.4927070614962}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:33+00","p50":217.5451108574219,"p95":769.7352407673627,"p99":782.5767544892922}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:34+00","p50":75.77212170904762,"p95":810.0876589894979,"p99":819.3601233074359}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:35+00","p50":146.05269533333336,"p95":796.4657528570968,"p99":808.4455695693895}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:36+00","p50":147.4019875789474,"p95":793.2937309405194,"p99":808.9293780171743}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:37+00","p50":116.47999470437405,"p95":824.802272002862,"p99":838.3714291738158}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:38+00","p50":160.93683846291208,"p95":831.692065454275,"p99":847.8802012906016}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:39+00","p50":162.61176531560022,"p95":858.7309830591663,"p99":876.5246451317278}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:40+00","p50":167.85274653609022,"p95":803.7985487810143,"p99":841.9492614244322}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:41+00","p50":195.49130067333334,"p95":845.6801240551719,"p99":856.0813435336237}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:42+00","p50":153.03157367439704,"p95":903.7474331152988,"p99":912.4517476887833}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:43+00","p50":128.97416448098008,"p95":892.338008125421,"p99":905.1434799806143}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:44+00","p50":151.508229824,"p95":815.6881297274264,"p99":823.5263541287617}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:45+00","p50":251.08404782291663,"p95":819.7828872825337,"p99":826.7838209971704}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:46+00","p50":123.05238415660375,"p95":838.1926180824704,"p99":850.9479638170691}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:47+00","p50":85.97893206510417,"p95":856.0561587034073,"p99":868.5477893461904}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:48+00","p50":89.90268073813954,"p95":870.3688951019693,"p99":886.4443832501291}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:49+00","p50":130.2606377762747,"p95":837.013389724972,"p99":847.4560089361693}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:50+00","p50":264.10521205463056,"p95":756.601415105787,"p99":771.9440873693303}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:51+00","p50":134.8368815096,"p95":830.1538964170584,"p99":851.8741183468301}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:52+00","p50":107.73757826639999,"p95":898.7922832942147,"p99":907.4230255254377}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:53+00","p50":167.85568099703704,"p95":837.3211568114643,"p99":843.996996953336}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:54+00","p50":244.9579556223907,"p95":839.8380977840637,"p99":850.7782102327152}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:55+00","p50":120.32408820833334,"p95":831.3844944967532,"p99":837.7835879033361}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:56+00","p50":189.06630808653847,"p95":906.9573108598827,"p99":921.1952591579208}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:57+00","p50":178.87932789285713,"p95":871.6791203076076,"p99":896.8697347445618}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:58+00","p50":137.53577148275863,"p95":894.9068849931532,"p99":911.6242372730751}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:11:59+00","p50":146.77376470646283,"p95":822.737283892796,"p99":840.4725975660258}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:00+00","p50":152.2565715328,"p95":818.937618893155,"p99":823.6658696460473}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:01+00","p50":180.4925367786458,"p95":814.7693117985966,"p99":827.1064605103194}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:02+00","p50":173.21156870600004,"p95":793.2962787663178,"p99":815.5747687904093}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:03+00","p50":175.709220248925,"p95":799.9829250406608,"p99":809.6611395335331}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:04+00","p50":196.74260295189504,"p95":782.2717043651278,"p99":800.9171513356547}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:05+00","p50":153.03618598830695,"p95":807.1243873598579,"p99":823.0264730749618}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:06+00","p50":76.77042728553792,"p95":797.7796690333925,"p99":806.5494952931116}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:07+00","p50":206.1038641925078,"p95":812.8454149081391,"p99":818.492905617795}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:08+00","p50":155.70031246930282,"p95":817.0086137099997,"p99":840.0536793729091}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:09+00","p50":113.43740190666668,"p95":828.490060390545,"p99":850.6972366327445}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:10+00","p50":113.08813453124999,"p95":798.1952301956919,"p99":805.9733666813413}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:11+00","p50":120.90239966521932,"p95":803.6998514344579,"p99":817.7640593754758}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:12+00","p50":234.44476295941723,"p95":816.4826719375952,"p99":830.8888692728224}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:13+00","p50":108.53925146648301,"p95":793.4580789526455,"p99":807.9570781699463}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:14+00","p50":94.40171745241301,"p95":774.5454579891942,"p99":781.5572883823908}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:15+00","p50":98.28205131221306,"p95":793.4014109024016,"p99":799.2534518847268}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:16+00","p50":149.91546381812498,"p95":818.7116649059312,"p99":845.8361067641156}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:17+00","p50":162.89186179166666,"p95":803.6888343996565,"p99":810.5711855771729}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:18+00","p50":114.44044262847223,"p95":850.9718695329565,"p99":878.191189075036}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:19+00","p50":112.27033090880002,"p95":884.9754953020279,"p99":893.1236864657036}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:20+00","p50":161.21657911683005,"p95":841.6200995233787,"p99":865.948623174376}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:21+00","p50":180.35841637602715,"p95":817.6500646699495,"p99":824.2686367378893}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:22+00","p50":184.16409523100936,"p95":823.204692636629,"p99":836.0479917125042}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:23+00","p50":151.29578651564628,"p95":825.2432267539327,"p99":843.5267358139216}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:24+00","p50":110.97868717621527,"p95":826.3428380570258,"p99":842.0019171795883}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:25+00","p50":124.76928563541667,"p95":814.4811531519808,"p99":830.4135864103738}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:26+00","p50":225.27264182758623,"p95":883.8346308824498,"p99":910.5547355746456}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:27+00","p50":145.7032308088954,"p95":884.2412410911751,"p99":898.6401220696649}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:28+00","p50":116.0750307967816,"p95":809.2419479355714,"p99":825.2028744525676}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:29+00","p50":88.61578418840138,"p95":773.9941811351388,"p99":780.3125210279703}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:30+00","p50":94.8484117890909,"p95":779.1257129434073,"p99":784.3368507415931}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:31+00","p50":87.88571630564104,"p95":793.8754665123143,"p99":804.206183968791}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:32+00","p50":209.25331669433598,"p95":782.7107842707007,"p99":789.6125400994349}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:33+00","p50":215.91457235680002,"p95":813.1666002687467,"p99":844.5037317605697}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:34+00","p50":306.00793250969525,"p95":998.7529788117207,"p99":1067.9282760886383}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:35+00","p50":249.67026980473378,"p95":638.016708325821,"p99":911.2827780391508}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:36+00","p50":154.381544626,"p95":252.50252235650564,"p99":310.132031204578}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:37+00","p50":183.3190920321739,"p95":319.4291874815696,"p99":365.06061010392233}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:38+00","p50":193.3142105008837,"p95":331.3361056195248,"p99":364.94288503852346}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:39+00","p50":149.37000596483517,"p95":263.24119990902614,"p99":295.262341940949}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:40+00","p50":253.95013613352242,"p95":392.0113868329774,"p99":403.8299803498166}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:41+00","p50":278.3650190621302,"p95":447.13465519822154,"p99":463.50688994675903}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:42+00","p50":327.0675881866667,"p95":549.1849150711822,"p99":565.908925497427}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:43+00","p50":154.346571625,"p95":663.960345165517,"p99":706.9689967292829}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:44+00","p50":142.91409854166668,"p95":749.0730854814415,"p99":753.605473168343}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:45+00","p50":212.90242085431842,"p95":794.5524503349382,"p99":807.668241748757}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:46+00","p50":146.10984501562498,"p95":740.2374836591185,"p99":788.7512185492161}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:47+00","p50":189.65458442424244,"p95":774.2880752316104,"p99":780.569189327636}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:48+00","p50":190.8069592512,"p95":746.8294023643793,"p99":757.9097181891154}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:49+00","p50":187.45036650499998,"p95":773.9806903844272,"p99":791.3742670580145}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:50+00","p50":160.58134735677086,"p95":748.4141391268612,"p99":776.1378920374683}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:51+00","p50":179.41724981944444,"p95":825.4117816150371,"p99":833.5722586341839}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:52+00","p50":193.8692579,"p95":817.4541948257579,"p99":838.3905615840354}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:53+00","p50":133.3131022506667,"p95":804.6272161971661,"p99":843.6754997437189}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:54+00","p50":215.23493519042972,"p95":820.1622156114123,"p99":836.576944936775}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:55+00","p50":195.98512836360152,"p95":829.5217399000771,"p99":846.9766035206576}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:56+00","p50":211.93463141326532,"p95":876.5158796463933,"p99":884.2941403455017}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:57+00","p50":194.95848175023127,"p95":889.8955760447503,"p99":898.061570316032}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:58+00","p50":174.8257035572917,"p95":836.983750345916,"p99":854.174595180014}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:12:59+00","p50":185.01765345281865,"p95":783.8091730162283,"p99":797.3781692199141}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:00+00","p50":181.49168468333332,"p95":854.0320286526222,"p99":876.3932843094778}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:01+00","p50":192.18683911134235,"p95":824.9003508866109,"p99":831.1554130373821}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:02+00","p50":212.040370654,"p95":760.2966647454697,"p99":775.1488676473422}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:03+00","p50":184.89962020105767,"p95":778.9522806104602,"p99":807.6623481844343}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:04+00","p50":180.41237994399998,"p95":875.1674407458381,"p99":885.0226158260291}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:05+00","p50":171.98483225868054,"p95":858.9723216030018,"p99":870.5855148745923}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:06+00","p50":174.83685631032387,"p95":834.8199987253341,"p99":846.0965595950023}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:07+00","p50":186.8001122653846,"p95":816.1589552005863,"p99":825.4137529974375}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:08+00","p50":134.82532062916667,"p95":894.2661401022333,"p99":903.7043637640378}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:09+00","p50":113.36263017777777,"p95":845.1683774860837,"p99":855.5202630538935}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:10+00","p50":111.29340879200001,"p95":863.6189820058019,"p99":868.6671017242558}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:11+00","p50":225.37168237602629,"p95":884.7661394545567,"p99":896.6839950083884}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:12+00","p50":109.51758659114581,"p95":907.8580082817858,"p99":915.461794684116}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:13+00","p50":133.42983011652422,"p95":913.161860423019,"p99":921.1865673666289}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:14+00","p50":160.99329049555553,"p95":868.8041385549701,"p99":899.1782039783062}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:15+00","p50":202.4228764336395,"p95":893.1561515892249,"p99":910.3530598796934}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:16+00","p50":113.26978362294315,"p95":873.8870630017905,"p99":887.9472598491782}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:17+00","p50":108.6087689952,"p95":905.1818828463887,"p99":919.0142638150488}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:18+00","p50":287.01552126415095,"p95":903.4711276134784,"p99":916.9875559992626}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:19+00","p50":197.20778676954737,"p95":943.9238810856001,"p99":958.6116163509146}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:20+00","p50":197.70403797110694,"p95":934.0908648802676,"p99":965.6466132295819}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:21+00","p50":182.26686428125,"p95":1122.0433041215838,"p99":1129.854995711439}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:22+00","p50":197.2527251426612,"p95":1063.0263271302263,"p99":1086.3412157802093}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:23+00","p50":258.81527862449053,"p95":882.4282529989281,"p99":896.4062585567303}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:24+00","p50":223.7080757692308,"p95":973.8024866058047,"p99":981.3237478547493}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:25+00","p50":192.59624074866306,"p95":926.3267635122235,"p99":960.2205222258593}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:26+00","p50":223.49669833680557,"p95":980.451540501403,"p99":1003.7237713220721}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:27+00","p50":195.43374572320005,"p95":1002.504648327415,"p99":1012.1461317605053}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:28+00","p50":166.28767430121528,"p95":963.0450437290514,"p99":974.2506227082052}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:29+00","p50":109.22427010377358,"p95":922.0160317599069,"p99":940.8509435786722}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:30+00","p50":105.85944407986112,"p95":890.5540513317512,"p99":903.7965199588152}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:31+00","p50":168.5451570805555,"p95":880.6942051721605,"p99":891.9372543461677}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:32+00","p50":166.77281305714286,"p95":854.7274366194209,"p99":872.8147343070067}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:33+00","p50":94.68549759712879,"p95":860.4162385056004,"p99":872.5838758471324}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:34+00","p50":133.48558659114585,"p95":927.8115824928457,"p99":937.2450700094437}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:35+00","p50":148.00545480851065,"p95":936.7433436246174,"p99":951.7217930557334}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:36+00","p50":162.989015019025,"p95":892.3388913348158,"p99":909.3859890199684}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:37+00","p50":180.40886922916147,"p95":900.0711100712679,"p99":916.2045711060756}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:38+00","p50":213.84058113333336,"p95":915.4224442869833,"p99":928.1269335484774}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:39+00","p50":241.36179240405798,"p95":854.0588794755464,"p99":879.4906479933715}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:40+00","p50":199.34068382639717,"p95":768.4692310106907,"p99":788.1794723503616}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:41+00","p50":187.43929888962583,"p95":809.9771460473406,"p99":834.5269188823062}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:42+00","p50":150.09477063234175,"p95":979.7069035564311,"p99":996.1765742936883}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:43+00","p50":157.27042085904762,"p95":967.9962700082426,"p99":1000.4057535846172}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:44+00","p50":133.57194086367346,"p95":946.5850482184725,"p99":953.629504281393}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:45+00","p50":159.42861509173787,"p95":934.7365016626865,"p99":946.8824579610449}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:46+00","p50":144.57733303030614,"p95":869.3214662252736,"p99":895.928362893282}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:47+00","p50":98.46951927701149,"p95":869.5757770250385,"p99":878.7699234011335}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:48+00","p50":275.77091145204173,"p95":939.9071615722406,"p99":950.0976703158276}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:49+00","p50":142.62724698350695,"p95":937.5853970026625,"p99":947.7411572172937}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:50+00","p50":186.5093194656,"p95":867.5459989119223,"p99":883.340993894637}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:51+00","p50":131.9369158515625,"p95":919.5471422079828,"p99":928.3176480177804}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:52+00","p50":111.77957655058825,"p95":984.2688748745799,"p99":993.8474432821193}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:53+00","p50":128.8260136753472,"p95":980.089137234283,"p99":1005.059262160181}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:54+00","p50":254.9861186135204,"p95":936.9383579013178,"p99":947.6925752817033}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:55+00","p50":197.0524846817559,"p95":893.9124456530994,"p99":949.0384812054104}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:56+00","p50":187.25692384190478,"p95":1054.843230726243,"p99":1071.1621746016642}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:57+00","p50":213.7206880740741,"p95":989.9125916148835,"p99":1025.40229471707}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:58+00","p50":139.2349521473684,"p95":1000.0495933011092,"p99":1010.0121380885872}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:13:59+00","p50":129.64639687500002,"p95":887.1540335826011,"p99":906.074784751306}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:00+00","p50":224.03850179886734,"p95":885.9223116361939,"p99":893.901546526556}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:01+00","p50":188.04045054166667,"p95":978.6861392026756,"p99":1007.200004677414}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:02+00","p50":125.36578821991903,"p95":1049.6987448359057,"p99":1060.9283521509722}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:03+00","p50":147.87167803472224,"p95":1014.8820879320615,"p99":1037.8197620287287}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:04+00","p50":164.2058836345486,"p95":1092.966333034545,"p99":1107.0748144059226}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:05+00","p50":222.24574280729166,"p95":1088.531539004985,"p99":1108.914955792625}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:06+00","p50":222.01065337573965,"p95":998.8524727065662,"p99":1009.2822465554979}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:07+00","p50":140.61986369432591,"p95":974.7979972075143,"p99":985.3659569811534}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:08+00","p50":191.32144759306124,"p95":997.6377500531552,"p99":1014.5865792375795}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:09+00","p50":229.7060846448,"p95":1036.9521699995291,"p99":1046.5717303484882}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:10+00","p50":223.822341648,"p95":987.7172072965918,"p99":1018.9593176868979}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:11+00","p50":174.3609642760417,"p95":1020.6294799501393,"p99":1030.6623873431547}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:12+00","p50":223.40259965919998,"p95":1024.2735574818262,"p99":1032.5917014920585}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:13+00","p50":192.94038563384615,"p95":1038.164579771017,"p99":1051.5459744437533}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:14+00","p50":163.56867076215278,"p95":1020.0403446379291,"p99":1036.5058942436958}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:15+00","p50":229.70329971556126,"p95":974.1025138264212,"p99":983.959001484522}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:16+00","p50":196.93356957546217,"p95":869.9085920611881,"p99":877.1828192817828}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:17+00","p50":174.43753960479998,"p95":880.3302723340369,"p99":887.9148995787999}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:18+00","p50":174.76491027024457,"p95":849.2061477959595,"p99":854.6386850625306}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:19+00","p50":218.6515476477366,"p95":907.0436274283328,"p99":924.7967792118858}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:20+00","p50":143.91287824382718,"p95":884.6718403451007,"p99":896.2685535886466}, + {"metric_name":"oidc_token_duration","timestamp":"2025-02-25 10:14:21+00","p50":188.35536131952665,"p95":861.5028502415789,"p99":885.7419178204723} +] diff --git a/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx new file mode 100644 index 0000000000..4615413d2b --- /dev/null +++ b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx @@ -0,0 +1,109 @@ +--- +title: oidc session benchmark of Zitadel v2.70.0 +sidebar_label: oidc session +--- + +The test implementats [Support for (OIDC) Standard in a Custom Login UI flow](https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard). + +The tests showed that querying the user takes too much time because Zitadel ensures the projection is up to date. This performance bottleneck must be resolved. + +## Performance test results + +| Metric | Value | +|:--------------------------------------|:------| +| Baseline | none | +| Purpose | Test current performance | +| Test start | 14:24 UTC | +| Test duration | 30min | +| Executed test | oidc\_session | +| k6 version | v0.57.0 | +| VUs | 600 | +| Client location | US1 | +| ZITADEL location | US1 | +| ZITADEL container specification | vCPU: 6
Memory: 6 Gi
Container min scale: 2
Container max scale: 7 | +| ZITADEL Version | v2.70.0 | +| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] | +| Database | type: psql
version: v17.2 | +| Database location | US1 | +| Database specification | vCPU: 8
memory: 32Gib | +| ZITADEL metrics during test | | +| Observed errors | | +| Top 3 most expensive database queries | 1: lock current\_states table
2: write events
3: get events for projection
| +| k6 Iterations per second | 153 | +| k6 output | [output](#k6-output) | +| flowchart outcome | Resolve locking issue | + +## Endpoint latencies + +import OutputSource from "!!raw-loader!./output.json"; + +import { BenchmarkChart } from '/src/components/benchmark_chart'; + + + +## k6 output {#k6-output} + +```bash + ✓ authorize status ok + ✓ auth request id returned + ✓ add Session status ok + ✓ finalize auth request status ok + + █ setup + + ✓ user defined + ✓ authorize status ok + ✓ login name status ok + ✓ login shows password page + ✓ password status ok + ✓ password callback + ✓ code set + ✓ token status ok + ✓ access token created + ✓ id token created + ✓ info created + ✓ org created + ✓ create user is status ok + ✓ generate machine key status ok + ✓ member added successful + ✓ openid configuration + ✓ access token returned + + █ teardown + + ✓ org removed + + checks...............................: 100.00% 1097103 out of 1097103 + data_received........................: 482 MB 267 kB/s + data_sent............................: 206 MB 114 kB/s + http_req_blocked.....................: min=150ns avg=185.63µs max=639.06ms p(50)=360ns p(95)=790ns p(99)=1.11µs + http_req_connecting..................: min=0s avg=76.84µs max=394.03ms p(50)=0s p(95)=0s p(99)=0s + http_req_duration....................: min=2.27ms avg=1.31s max=6.57s p(50)=326.44ms p(95)=3.94s p(99)=4.28s + { expected_response:true }.........: min=2.27ms avg=1.31s max=6.57s p(50)=326.44ms p(95)=3.94s p(99)=4.28s + http_req_failed......................: 0.00% 0 out of 823429 + http_req_receiving...................: min=22.92µs avg=143.73µs max=245.98ms p(50)=105.17µs p(95)=188.26µs p(99)=260.56µs + http_req_sending.....................: min=22.37µs avg=67.8µs max=41.57ms p(50)=63.65µs p(95)=104.8µs p(99)=138.46µs + http_req_tls_handshaking.............: min=0s avg=106.12µs max=580.5ms p(50)=0s p(95)=0s p(99)=0s + http_req_waiting.....................: min=2.11ms avg=1.31s max=6.57s p(50)=326.17ms p(95)=3.94s p(99)=4.28s + http_reqs............................: 823429 456.440453/s + iteration_duration...................: min=713.37ms avg=3.94s max=8.94s p(50)=3.92s p(95)=4.98s p(99)=5.44s + iterations...........................: 274271 152.032998/s + login_ui_enter_login_name_duration...: min=113.75ms avg=113.75ms max=113.75ms p(50)=113.75ms p(95)=113.75ms p(99)=113.75ms + login_ui_enter_password_duration.....: min=2.27ms avg=2.27ms max=2.27ms p(50)=2.27ms p(95)=2.27ms p(99)=2.27ms + login_ui_init_login_duration.........: min=20.48ms avg=156.67ms max=6.57s p(50)=126.64ms p(95)=280.16ms p(99)=675.36ms + login_ui_token_duration..............: min=68.53ms avg=68.53ms max=68.53ms p(50)=68.53ms p(95)=68.53ms p(99)=68.53ms + membership_iam_member................: min=34.16ms avg=34.16ms max=34.16ms p(50)=34.16ms p(95)=34.16ms p(99)=34.16ms + oidc_auth_requst_by_id_duration......: min=20.59ms avg=370.56ms max=2.87s p(50)=294.12ms p(95)=911.42ms p(99)=1.08s + oidc_session_duration................: min=713ms avg=3.94s max=8.94s p(50)=3.92s p(95)=4.98s p(99)=5.44s + oidc_token_duration..................: min=40.67ms avg=40.67ms max=40.67ms p(50)=40.67ms p(95)=40.67ms p(99)=40.67ms + org_create_org_duration..............: min=48.92ms avg=48.92ms max=48.92ms p(50)=48.92ms p(95)=48.92ms p(99)=48.92ms + session_add_session_duration.........: min=92.04ms avg=3.4s max=6.16s p(50)=3.46s p(95)=4.18s p(99)=4.48s + user_add_machine_key_duration........: min=32.08ms avg=32.08ms max=32.08ms p(50)=32.08ms p(95)=32.08ms p(99)=32.08ms + user_create_machine_duration.........: min=91.73ms avg=91.73ms max=91.73ms p(50)=91.73ms p(95)=91.73ms p(99)=91.73ms + vus..................................: 82 min=0 max=600 + vus_max..............................: 600 min=600 max=600 + + +running (30m04.0s), 000/600 VUs, 274271 complete and 0 interrupted iterations +default ✓ [======================================] 600 VUs 30m0s +``` diff --git a/docs/docs/apis/benchmarks/v2.70.0/oidc_session/output.json b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/output.json new file mode 100644 index 0000000000..89e85dcb68 --- /dev/null +++ b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/output.json @@ -0,0 +1,7213 @@ +[ + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:23:57+00","p50":62.339445,"p95":62.339445,"p99":62.339445}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:23:58+00","p50":258.77074200000004,"p95":304.8193776022043,"p99":307.750769}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:23:59+00","p50":612.5886873333334,"p95":1051.252312772991,"p99":1150.3667293036326}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:00+00","p50":1466.34658,"p95":1890.521405622621,"p99":1966.6285925087814}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:01+00","p50":2229.766913,"p95":2858.042111162243,"p99":3013.352476644381}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:02+00","p50":2936.8253113749997,"p95":3723.0829172441677,"p99":3864.348713233706}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:03+00","p50":2722.70931,"p95":4551.176884346697,"p99":4884.324276698396}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:04+00","p50":2403.494922875,"p95":4134.315002882077,"p99":5349.750400102871}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:05+00","p50":2425.6356775,"p95":4596.048507064533,"p99":6478.252107770193}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:06+00","p50":1809.1544685000001,"p95":3456.0459715700836,"p99":4211.548926564352}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:07+00","p50":2300.46873375,"p95":3778.969410980997,"p99":5132.441662292213}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:08+00","p50":2022.036871625,"p95":4286.522664230069,"p99":4978.1999302477}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:09+00","p50":1763.95020875,"p95":3942.855478062964,"p99":5376.54929871872}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:10+00","p50":1497.416847125,"p95":2532.823232301905,"p99":3163.63542474717}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:11+00","p50":1510.00130775,"p95":2443.4107665183856,"p99":3815.048016467437}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:12+00","p50":1423.3122075,"p95":2403.431203517892,"p99":2933.722435813967}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:13+00","p50":1093.10377325,"p95":1735.8192317266098,"p99":2462.717855297638}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:14+00","p50":988.92042625,"p95":1677.5313742469152,"p99":2136.529088371923}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:15+00","p50":774.25359525,"p95":1330.24933740759,"p99":1585.9313650582524}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:16+00","p50":671.99325975,"p95":1165.0208613960456,"p99":1302.1564326984715}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:17+00","p50":339.485701,"p95":697.3261753768692,"p99":843.3945306105576}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:18+00","p50":201.913483375,"p95":405.01662332725147,"p99":493.8774995468173}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:19+00","p50":80.622624,"p95":176.80868078289902,"p99":209.23326252017404}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:20+00","p50":94.54975825,"p95":166.80380294345045,"p99":229.63215406687831}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:21+00","p50":134.84770025,"p95":249.96121227380942,"p99":304.6364869858971}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:22+00","p50":135.851149625,"p95":243.7183858047975,"p99":271.48962011084393}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:23+00","p50":158.864069,"p95":300.3324957839335,"p99":361.86525808865025}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:24+00","p50":153.24412925,"p95":271.6212738847816,"p99":410.62831231663944}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:25+00","p50":102.7337625,"p95":189.38235534318898,"p99":216.43320800002382}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:26+00","p50":112.08041475,"p95":199.91836140540218,"p99":258.29287847357557}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:27+00","p50":147.3408945,"p95":301.40192853005624,"p99":367.25708713191847}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:28+00","p50":249.730147,"p95":437.3320270258285,"p99":476.21646201493314}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:29+00","p50":155.48859487500002,"p95":307.8827681803662,"p99":434.3487465989096}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:30+00","p50":81.205484875,"p95":160.0606962501876,"p99":194.8828979206083}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:31+00","p50":99.88190674999998,"p95":174.38744338783283,"p99":197.63848377611234}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:32+00","p50":122.605796,"p95":220.9572788749256,"p99":250.8256070917764}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:33+00","p50":132.900805375,"p95":233.9873465163738,"p99":278.4107434359565}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:34+00","p50":116.745638625,"p95":205.99698638853226,"p99":272.9563828046458}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:35+00","p50":120.912261,"p95":210.90340303533114,"p99":224.24648841782954}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:36+00","p50":121.429260125,"p95":205.73814434296582,"p99":223.5805043241348}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:37+00","p50":133.60109187499998,"p95":222.34477844290518,"p99":312.3835695351458}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:38+00","p50":138.033385125,"p95":245.3691680875998,"p99":306.79640918104025}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:39+00","p50":98.761413,"p95":208.16198174989222,"p99":253.05098553597642}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:40+00","p50":128.428046,"p95":239.9824594242184,"p99":269.22985423001865}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:41+00","p50":128.3695035,"p95":245.3018024707196,"p99":319.29224956482983}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:42+00","p50":163.64383049999998,"p95":268.6600551537396,"p99":294.01614245344115}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:43+00","p50":139.71572725,"p95":254.93232634073448,"p99":303.9782826224594}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:44+00","p50":104.160599125,"p95":189.62585153889592,"p99":216.47245810448385}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:45+00","p50":71.68220725,"p95":143.28919135566616,"p99":177.8357330769234}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:46+00","p50":145.61508175,"p95":258.58472171387615,"p99":301.66242926791097}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:47+00","p50":122.6830735,"p95":255.11650307970416,"p99":297.04778025715683}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:48+00","p50":148.91887175,"p95":275.2239237879314,"p99":316.5770543267822}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:49+00","p50":88.96301712500001,"p95":161.57574872134887,"p99":187.1681947913649}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:50+00","p50":81.3965965,"p95":147.8346919073019,"p99":201.03919433998107}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:51+00","p50":120.65103400000001,"p95":265.45174884270523,"p99":296.96692228469277}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:52+00","p50":127.27438849999999,"p95":307.58304463195014,"p99":425.0258861029873}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:53+00","p50":113.021582,"p95":219.83732407803558,"p99":316.9884570452313}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:54+00","p50":113.07502837499999,"p95":204.1461799367106,"p99":255.23649308430933}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:55+00","p50":131.36962599999998,"p95":215.36513332501673,"p99":260.0590691196408}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:56+00","p50":165.88236575,"p95":330.7391823178491,"p99":425.51631017698384}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:57+00","p50":162.9484785,"p95":300.4033115443126,"p99":352.63109168887996}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:58+00","p50":209.762295,"p95":337.76112798572564,"p99":402.5020439606638}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:24:59+00","p50":154.950731125,"p95":357.53976211221806,"p99":410.1808560318742}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:00+00","p50":95.46575175,"p95":165.9329520700996,"p99":193.19430885511733}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:01+00","p50":110.32243199999999,"p95":205.02609343541116,"p99":238.3471862522974}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:02+00","p50":108.69780775000001,"p95":180.4847667307223,"p99":211.47397537223412}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:03+00","p50":126.999021,"p95":220.27880752573287,"p99":275.1039267062788}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:04+00","p50":103.47961987500001,"p95":198.3098625562466,"p99":255.211529183651}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:05+00","p50":97.769744,"p95":157.10999089385137,"p99":185.0256985644598}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:06+00","p50":120.41201725,"p95":209.90686408629654,"p99":243.03370417965172}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:07+00","p50":100.90729775,"p95":186.82457776383313,"p99":239.67428486025477}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:08+00","p50":108.608282,"p95":205.69547052944756,"p99":235.1612237323804}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:09+00","p50":98.440318,"p95":166.9628958964776,"p99":190.1057014537153}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:10+00","p50":121.45391112499999,"p95":201.66206300973536,"p99":244.47408824865295}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:11+00","p50":169.99164150000001,"p95":306.78302976255185,"p99":369.4129249820883}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:12+00","p50":173.96470475,"p95":314.173019101388,"p99":360.68249911093517}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:13+00","p50":90.248065125,"p95":193.93462233031963,"p99":223.7667799232671}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:14+00","p50":117.284639,"p95":283.2187083831297,"p99":312.54090032681324}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:15+00","p50":209.89915975000002,"p95":357.8105314326649,"p99":458.3502724096184}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:16+00","p50":175.668555,"p95":408.30996491104725,"p99":454.3367659646912}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:17+00","p50":126.53598225,"p95":244.5066677826625,"p99":284.96427947670486}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:18+00","p50":182.585486,"p95":300.4480899116068,"p99":415.43282852241134}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:19+00","p50":85.036164,"p95":194.0201562075404,"p99":257.17420427080987}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:20+00","p50":110.99374399999999,"p95":182.93796673325633,"p99":210.99211090101625}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:21+00","p50":158.27253075,"p95":280.8447603797603,"p99":406.3895710498381}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:22+00","p50":131.073961,"p95":306.1104988804932,"p99":390.5669742125015}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:23+00","p50":120.18386774999999,"p95":225.6113125841849,"p99":266.6700864262009}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:24+00","p50":83.811248125,"p95":153.91033095073067,"p99":229.97063926745008}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:25+00","p50":104.288765375,"p95":265.04793333687,"p99":781.361796479462}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:26+00","p50":87.991764625,"p95":192.72405145385153,"p99":277.11009253364773}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:27+00","p50":79.24215650000001,"p95":140.30139122265007,"p99":181.44180362664295}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:28+00","p50":141.91416325,"p95":221.03828344602192,"p99":258.52044517297696}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:29+00","p50":115.37705625,"p95":234.6432947635988,"p99":271.933878975307}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:30+00","p50":109.40425950000001,"p95":209.50474681511835,"p99":259.56671240850136}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:31+00","p50":136.66431624999998,"p95":242.32203516944003,"p99":291.9124370187953}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:32+00","p50":104.87810250000001,"p95":215.7881174466953,"p99":359.04249330302906}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:33+00","p50":121.87192825,"p95":236.49695988608073,"p99":336.3721206654472}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:34+00","p50":175.74734125,"p95":323.71743514348844,"p99":409.43231308774614}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:35+00","p50":166.974268875,"p95":305.019184363189,"p99":353.04063514338225}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:36+00","p50":200.39391525000002,"p95":319.16844496324785,"p99":389.91729644082903}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:37+00","p50":103.06974724999999,"p95":395.2213094524536,"p99":433.94111075871274}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:38+00","p50":145.86402950000002,"p95":255.59114736909297,"p99":353.3783478235779}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:39+00","p50":121.8788085,"p95":208.96086631138468,"p99":235.55708836210346}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:40+00","p50":112.94661,"p95":205.92689588133533,"p99":246.81114772996378}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:41+00","p50":118.36935562499998,"p95":199.87209803065448,"p99":242.7695993302276}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:42+00","p50":93.162139375,"p95":154.2165233394804,"p99":169.03084851870108}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:43+00","p50":172.78262425,"p95":329.82226271698124,"p99":451.04865585016825}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:44+00","p50":170.502716,"p95":328.0731294097156,"p99":372.06686009350176}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:45+00","p50":108.1077565,"p95":187.81222636979376,"p99":216.76844491746806}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:46+00","p50":124.531436,"p95":202.42340565270806,"p99":226.85033417475128}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:47+00","p50":147.61186650000002,"p95":246.20052270657277,"p99":288.1365970182802}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:48+00","p50":155.049870125,"p95":261.5078678293365,"p99":322.147174874124}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:49+00","p50":162.05682325,"p95":280.1764878707628,"p99":325.851279985611}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:50+00","p50":162.02391825,"p95":272.60000720487113,"p99":358.81135469753934}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:51+00","p50":203.6982145,"p95":349.5975435165336,"p99":454.6759331243286}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:52+00","p50":236.5626475,"p95":429.5447336779504,"p99":454.5021117719574}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:53+00","p50":233.40121375,"p95":394.48415033848266,"p99":487.2311414028055}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:54+00","p50":104.500477375,"p95":225.8710205311589,"p99":339.7188048663127}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:55+00","p50":84.47709825000001,"p95":200.53741230225086,"p99":252.3193293331108}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:56+00","p50":176.535817,"p95":305.1437717488809,"p99":327.8969502522719}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:57+00","p50":124.00651074999999,"p95":207.40135342886614,"p99":274.86053937365244}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:58+00","p50":121.298443625,"p95":202.67407245358348,"p99":275.77927709895374}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:25:59+00","p50":164.774263625,"p95":252.70300307483052,"p99":314.27873929903694}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:00+00","p50":146.396086,"p95":266.84947615611566,"p99":287.55821154191403}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:01+00","p50":121.873736,"p95":213.2060694931152,"p99":278.46537486591717}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:02+00","p50":135.90665475,"p95":247.38943783265447,"p99":323.91765247279835}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:03+00","p50":115.51458525,"p95":230.91915040398175,"p99":303.65039661653475}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:04+00","p50":137.90411,"p95":239.5515597439022,"p99":301.0728532459335}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:05+00","p50":121.81873925,"p95":189.82104425847388,"p99":275.1509595048299}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:06+00","p50":117.64341350000001,"p95":185.63193928801275,"p99":221.3588303736348}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:07+00","p50":135.37097925,"p95":205.95409413840676,"p99":246.17206610061265}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:08+00","p50":129.892028,"p95":247.24903646966757,"p99":336.33620865296837}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:09+00","p50":161.538913,"p95":267.4152007579935,"p99":342.07675686108877}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:10+00","p50":80.07702925000001,"p95":164.80643884602736,"p99":196.44627083702088}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:11+00","p50":128.1238875,"p95":230.56911746719354,"p99":259.92257737622043}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:12+00","p50":131.93008375,"p95":237.19878021039668,"p99":265.17925049911497}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:13+00","p50":80.92376875,"p95":166.19935815821123,"p99":218.63094601485633}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:14+00","p50":107.20864575,"p95":183.9416062268815,"p99":218.68593009438493}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:15+00","p50":118.849626,"p95":211.47769716343677,"p99":227.38683597412373}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:16+00","p50":161.23569500000002,"p95":286.204805217598,"p99":343.8168104247828}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:17+00","p50":190.55114075,"p95":328.5241270366991,"p99":398.16121423104954}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:18+00","p50":152.96415100000002,"p95":288.0607910135304,"p99":333.71225881917735}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:19+00","p50":114.50825375,"p95":199.46055038071154,"p99":342.020087477211}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:20+00","p50":122.31746225,"p95":238.2758807797658,"p99":348.1809812281346}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:21+00","p50":145.187960375,"p95":312.02423999221065,"p99":373.39085369808436}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:22+00","p50":104.4269945,"p95":216.50670777161824,"p99":250.691692627326}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:23+00","p50":93.854250125,"p95":180.63569997648548,"p99":195.76191512520884}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:24+00","p50":162.52621299999998,"p95":305.1106610165293,"p99":421.2744577359433}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:25+00","p50":90.46220875,"p95":155.03327919440764,"p99":191.43126126426125}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:26+00","p50":121.42907225,"p95":259.0407722422349,"p99":300.4013097425168}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:27+00","p50":191.489638,"p95":338.46886518697704,"p99":409.9304658424754}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:28+00","p50":254.091921,"p95":498.5261794089118,"p99":652.4870352712064}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:29+00","p50":195.65243262499996,"p95":359.5853661030371,"p99":434.67802980742096}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:30+00","p50":104.37752474999999,"p95":189.11078112564564,"p99":226.1139453166275}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:31+00","p50":128.52471275,"p95":275.020431605122,"p99":337.4948282176714}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:32+00","p50":111.99377387499999,"p95":205.45506149177308,"p99":283.90608798797416}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:33+00","p50":92.952551125,"p95":178.08582774648798,"p99":217.53181213558767}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:34+00","p50":128.96273975000003,"p95":216.26248524941136,"p99":255.0496386518245}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:35+00","p50":80.92732125,"p95":196.68805005272628,"p99":275.4611588623581}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:36+00","p50":130.0291,"p95":280.3884509568725,"p99":328.3164207868519}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:37+00","p50":152.59994375,"p95":263.5131926977873,"p99":292.10753289325}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:38+00","p50":90.19333325,"p95":185.97320981996154,"p99":219.97406425438024}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:39+00","p50":133.452378375,"p95":239.92717295034717,"p99":305.6644703736977}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:40+00","p50":126.000802625,"p95":229.96900222730343,"p99":279.7686764251833}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:41+00","p50":157.95002825,"p95":282.8278621401409,"p99":338.0009739066853}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:42+00","p50":155.87846575,"p95":282.81779471820755,"p99":319.075486067091}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:43+00","p50":84.791211875,"p95":201.00873207962212,"p99":265.1927523357043}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:44+00","p50":131.13657,"p95":232.9998295103712,"p99":267.1778742193985}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:45+00","p50":69.03678837499999,"p95":123.14367056466914,"p99":146.17256146747852}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:46+00","p50":71.84918975,"p95":125.29048634898186,"p99":161.22569621515655}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:47+00","p50":108.3962715,"p95":189.21288241272092,"p99":250.43372209188507}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:48+00","p50":173.589551625,"p95":340.32142986462975,"p99":435.0298410092254}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:49+00","p50":145.074171375,"p95":278.80661311543963,"p99":322.5748526054079}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:50+00","p50":109.930184875,"p95":205.2679639904865,"p99":234.0642826333468}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:51+00","p50":79.708421875,"p95":150.68225488877206,"p99":190.87135977002572}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:52+00","p50":113.259191,"p95":216.83242306798743,"p99":260.34773490463255}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:53+00","p50":144.34075025,"p95":264.9229432889909,"p99":292.3955839349995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:54+00","p50":129.49980950000003,"p95":228.63573991439424,"p99":270.81280549609704}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:55+00","p50":161.749598,"p95":266.27565269464526,"p99":298.95324273064807}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:56+00","p50":161.51081775,"p95":289.8261035301765,"p99":312.44927181175444}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:57+00","p50":144.680062375,"p95":251.73008695947266,"p99":319.2730107494321}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:58+00","p50":103.74432949999999,"p95":234.07673280859632,"p99":262.35760736007114}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:26:59+00","p50":98.359315875,"p95":171.64733912472855,"p99":205.86780048810624}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:00+00","p50":224.19412749999998,"p95":430.5284287400694,"p99":472.500818028223}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:01+00","p50":214.94447825,"p95":332.4069813295262,"p99":491.1537392900348}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:02+00","p50":87.151083,"p95":149.74296313370849,"p99":276.8641602423944}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:03+00","p50":103.007207,"p95":179.4887288067106,"p99":216.45315720721055}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:04+00","p50":181.669376,"p95":327.77605005206107,"p99":393.2713689738388}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:05+00","p50":129.611630125,"p95":338.52021778444407,"p99":413.33721826308704}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:06+00","p50":135.57536050000002,"p95":241.09002528889812,"p99":263.1779390573251}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:07+00","p50":117.5742735,"p95":225.33277177588855,"p99":284.7057558166418}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:08+00","p50":106.05203699999998,"p95":203.89150652199532,"p99":264.5495631084714}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:09+00","p50":140.008549125,"p95":246.17656151906877,"p99":296.20616681778927}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:10+00","p50":170.39995750000003,"p95":314.5208791921652,"p99":449.7122427047903}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:11+00","p50":148.487858625,"p95":237.55055619155775,"p99":263.07055659813665}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:12+00","p50":97.5083895,"p95":237.45036286930443,"p99":311.6070163106155}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:13+00","p50":95.2986335,"p95":182.7446142193631,"p99":223.1338157649808}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:14+00","p50":134.70301325,"p95":238.70017526986868,"p99":274.75862380349946}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:15+00","p50":155.4862305,"p95":286.4197492095982,"p99":330.7174139394374}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:16+00","p50":157.2371905,"p95":344.28687604996253,"p99":436.2257932693644}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:17+00","p50":182.28017849999998,"p95":337.41122012631547,"p99":422.3947210080943}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:18+00","p50":280.8489555,"p95":465.9044066284709,"p99":534.709425199831}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:19+00","p50":212.039927,"p95":412.8942489033137,"p99":487.0306148927772}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:20+00","p50":85.27730825,"p95":161.17831913523864,"p99":218.57807443371962}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:21+00","p50":146.1264885,"p95":280.43152174114454,"p99":328.1703315011897}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:22+00","p50":154.2236105,"p95":287.3407274379973,"p99":343.0989736846919}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:23+00","p50":106.63604199999999,"p95":214.39217658339106,"p99":254.3369112411437}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:24+00","p50":133.998239,"p95":250.71500307920837,"p99":314.97762923251344}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:25+00","p50":160.1628925,"p95":274.95658198348247,"p99":325.88719703962994}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:26+00","p50":191.39069775000002,"p95":304.87406217246627,"p99":372.72260921007535}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:27+00","p50":158.012422875,"p95":243.3969006114224,"p99":288.9334179496391}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:28+00","p50":139.386489,"p95":252.27419602433568,"p99":280.0468905361411}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:29+00","p50":123.69265375,"p95":208.1789062341764,"p99":244.88187368466947}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:30+00","p50":113.095934875,"p95":182.65724506324804,"p99":225.4004828125377}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:31+00","p50":108.2514075,"p95":201.32596045630282,"p99":279.4491642193556}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:32+00","p50":151.30075025000002,"p95":338.3215411476516,"p99":435.7161857847447}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:33+00","p50":169.178243125,"p95":276.7689444595258,"p99":341.75094073169754}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:34+00","p50":126.728785,"p95":276.16515665615873,"p99":378.9622413867683}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:35+00","p50":113.951581,"p95":236.23333125094842,"p99":252.57109136116028}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:36+00","p50":118.91571724999999,"p95":224.42526988624024,"p99":272.2253195049958}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:37+00","p50":117.97213550000001,"p95":239.48008170637888,"p99":283.9768771665761}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:38+00","p50":98.715278,"p95":161.3067124477978,"p99":224.08999260514832}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:39+00","p50":104.3048115,"p95":191.47667405479314,"p99":230.81905961741165}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:40+00","p50":128.67685999999998,"p95":224.3690010760014,"p99":296.6125738251982}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:41+00","p50":184.1455805,"p95":319.0163887965636,"p99":388.34378445272733}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:42+00","p50":167.557597,"p95":303.76941795591677,"p99":373.23217727749966}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:43+00","p50":148.56774475,"p95":266.6482720201388,"p99":308.3764299881537}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:44+00","p50":138.6539045,"p95":242.81603112916923,"p99":326.60747415434736}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:45+00","p50":140.51406425,"p95":252.37621297669028,"p99":274.260405402298}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:46+00","p50":117.606172625,"p95":177.0593152675221,"p99":220.58356091637634}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:47+00","p50":144.1524865,"p95":253.8820211348152,"p99":285.6416172647114}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:48+00","p50":134.8931645,"p95":214.73377863118662,"p99":271.803070198863}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:49+00","p50":84.747053,"p95":183.7514163944516,"p99":231.84371373550127}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:50+00","p50":91.97071425,"p95":161.16665137580287,"p99":184.45860150095393}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:51+00","p50":129.00125575,"p95":253.52428303538733,"p99":287.5147433752444}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:52+00","p50":120.75225075,"p95":238.12202377890952,"p99":289.0132848020582}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:53+00","p50":72.8180925,"p95":275.5048755232887,"p99":333.01630285324524}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:54+00","p50":166.813467,"p95":287.252265096695,"p99":425.6935347129059}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:55+00","p50":152.11969925,"p95":275.6256685159703,"p99":408.3138829792819}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:56+00","p50":125.68134875000001,"p95":219.99952303352313,"p99":283.80332375073743}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:57+00","p50":103.59044450000002,"p95":206.80742811902303,"p99":240.64250724737047}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:58+00","p50":142.747529,"p95":247.2968077442006,"p99":304.6619194760995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:27:59+00","p50":145.04037812500002,"p95":259.3552853584927,"p99":322.3908203913068}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:00+00","p50":116.896941,"p95":253.69721873492634,"p99":327.36586049640084}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:01+00","p50":117.491841,"p95":200.16130201312328,"p99":249.40491876453018}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:02+00","p50":141.10933625,"p95":292.7466525136427,"p99":360.57083601154807}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:03+00","p50":101.56465937499999,"p95":196.6713329223383,"p99":212.75610551703264}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:04+00","p50":184.04186912499995,"p95":278.9518956062697,"p99":331.1907442709555}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:05+00","p50":166.034122,"p95":302.49065924267984,"p99":337.1064949128821}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:06+00","p50":125.5577355,"p95":221.68361882716155,"p99":294.68616576544287}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:07+00","p50":79.27365387500001,"p95":170.1143819094808,"p99":215.02191827812314}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:08+00","p50":189.48967449999998,"p95":313.0046311657138,"p99":372.2441024757123}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:09+00","p50":175.61609937499998,"p95":261.3699798656588,"p99":352.30132371636347}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:10+00","p50":151.69483925,"p95":275.3367951394992,"p99":301.64116372481203}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:11+00","p50":97.3218975,"p95":186.81788814242665,"p99":240.16954303669954}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:12+00","p50":192.67417062500002,"p95":292.60155422256656,"p99":340.9740105527825}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:13+00","p50":176.3939085,"p95":328.8460191401305,"p99":453.8221228941927}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:14+00","p50":132.0770625,"p95":245.69620103810072,"p99":287.5159258611154}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:15+00","p50":96.6422825,"p95":162.57929904904304,"p99":203.58991057710074}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:16+00","p50":107.85360812500001,"p95":194.7230699357515,"p99":230.92471614653158}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:17+00","p50":119.388885875,"p95":198.01640603724988,"p99":248.92444979705883}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:18+00","p50":106.84148637500002,"p95":196.41971112324367,"p99":237.10626086190558}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:19+00","p50":94.3971075,"p95":177.6103008411679,"p99":222.30656479013538}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:20+00","p50":85.44688049999999,"p95":150.8254637445855,"p99":169.5895655597744}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:21+00","p50":90.70992799999999,"p95":169.6062778058667,"p99":205.73066347210215}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:22+00","p50":121.1873585,"p95":217.12619933547782,"p99":263.40825334547424}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:23+00","p50":157.61027575,"p95":272.4139090822719,"p99":347.0562172099047}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:24+00","p50":79.1193135,"p95":171.703820723088,"p99":214.20058660788345}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:25+00","p50":199.418836375,"p95":337.0903869744407,"p99":382.6705857383341}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:26+00","p50":197.83513837499999,"p95":413.3557164345593,"p99":524.2143513585806}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:27+00","p50":129.1172775,"p95":236.02128140383456,"p99":324.30315063636544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:28+00","p50":143.00698975,"p95":278.94960529228064,"p99":311.65795859507443}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:29+00","p50":101.42420175,"p95":162.23032102305604,"p99":191.75841195425988}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:30+00","p50":123.37847375,"p95":233.42535071152943,"p99":297.9504201772401}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:31+00","p50":107.955679,"p95":187.4649409562993,"p99":218.78511079152847}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:32+00","p50":85.30807075,"p95":147.12308162440465,"p99":170.7508150957675}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:33+00","p50":96.79359475,"p95":184.35878904683113,"p99":243.53863540313722}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:34+00","p50":125.06368125,"p95":249.4197935131814,"p99":328.9112866527493}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:35+00","p50":129.223492,"p95":229.83890892393063,"p99":313.8353336180143}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:36+00","p50":122.01853,"p95":226.84750504872548,"p99":263.09705869331026}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:37+00","p50":98.948057,"p95":178.37118210773087,"p99":213.47674875252773}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:38+00","p50":144.52986900000002,"p95":232.26404542651392,"p99":267.00160386983396}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:39+00","p50":91.19648124999999,"p95":217.44530391093855,"p99":307.066786078295}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:40+00","p50":86.31139125000001,"p95":153.99202094052487,"p99":191.9769022891071}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:41+00","p50":122.12291775,"p95":194.36299235924577,"p99":230.74981155183028}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:42+00","p50":84.67345175,"p95":153.66712516805995,"p99":207.4707485197773}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:43+00","p50":107.5286885,"p95":187.94218008179664,"p99":216.05675935889053}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:44+00","p50":91.386695375,"p95":176.7614089843783,"p99":223.6953505829129}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:45+00","p50":94.26895875,"p95":165.36120567740028,"p99":190.0989624462712}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:46+00","p50":140.12817375,"p95":294.557461591749,"p99":314.8331035929489}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:47+00","p50":83.27134325,"p95":141.18936033415127,"p99":159.48423490322875}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:48+00","p50":100.56725812500001,"p95":210.11579954620709,"p99":260.94590228436255}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:49+00","p50":169.055963625,"p95":301.5246726097504,"p99":393.6182446898877}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:50+00","p50":189.70676874999998,"p95":358.15270057301683,"p99":409.6520169406807}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:51+00","p50":146.7766985,"p95":288.81404874374994,"p99":314.1154399872408}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:52+00","p50":148.01201175,"p95":280.3084735530743,"p99":304.9361293612933}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:53+00","p50":144.27042849999998,"p95":252.99522648699985,"p99":335.75158685156487}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:54+00","p50":118.56673275,"p95":220.60333793239033,"p99":290.3112148919711}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:55+00","p50":127.88203362499999,"p95":226.9018506400673,"p99":263.55477784556984}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:56+00","p50":135.095340375,"p95":247.99795177745585,"p99":283.1354580115335}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:57+00","p50":118.788546375,"p95":202.9921249502286,"p99":247.6886837669313}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:58+00","p50":111.937216,"p95":186.10904270538214,"p99":233.25000319811343}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:28:59+00","p50":137.43512275,"p95":267.40894726768363,"p99":326.5315515065327}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:00+00","p50":107.745891125,"p95":176.6703956931873,"p99":234.48687078047442}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:01+00","p50":108.948009875,"p95":198.71243050037288,"p99":227.46675437707827}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:02+00","p50":134.658324,"p95":224.14865123099756,"p99":251.88450463898278}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:03+00","p50":187.209733625,"p95":311.0892797334904,"p99":361.0140368406591}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:04+00","p50":102.32390125,"p95":186.5861975439346,"p99":254.97673778805446}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:05+00","p50":91.9990925,"p95":169.1640546961567,"p99":223.23189777210186}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:06+00","p50":95.3258525,"p95":164.91076562355232,"p99":179.5392866664276}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:07+00","p50":101.52308275000001,"p95":189.81198168373115,"p99":221.04319105974864}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:08+00","p50":113.37405825,"p95":212.58137224085783,"p99":261.703685465714}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:09+00","p50":112.163341,"p95":196.3774321120682,"p99":238.18226578650953}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:10+00","p50":169.51515375,"p95":332.054849600981,"p99":353.63174300643254}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:11+00","p50":171.0175385,"p95":278.9449999355333,"p99":303.030006034503}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:12+00","p50":109.27713875,"p95":213.07570373858684,"p99":248.68207259891986}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:13+00","p50":109.30497875,"p95":247.0847239415872,"p99":339.7349968072023}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:14+00","p50":103.506105375,"p95":180.40844785259398,"p99":214.81278446597173}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:15+00","p50":141.51360825,"p95":240.46166829774677,"p99":281.0931983415356}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:16+00","p50":135.746889,"p95":217.23297700920426,"p99":281.7314135155401}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:17+00","p50":162.64720950000003,"p95":272.3121158818294,"p99":307.08627023656345}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:18+00","p50":173.244071,"p95":305.5436752984572,"p99":384.3586763511896}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:19+00","p50":147.00583924999998,"p95":262.7795125512412,"p99":302.3056161814046}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:20+00","p50":123.155491375,"p95":238.8820221819256,"p99":350.34427757199643}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:21+00","p50":146.54138575,"p95":256.9378296471801,"p99":293.3194825697508}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:22+00","p50":181.59883,"p95":322.2476052710166,"p99":362.8304313739882}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:23+00","p50":119.9760205,"p95":195.42988882607423,"p99":263.340930191}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:24+00","p50":166.022541625,"p95":251.22756778456534,"p99":309.2453509117308}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:25+00","p50":111.136131375,"p95":188.680955581707,"p99":217.4910119656749}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:26+00","p50":154.613173,"p95":265.73202318982413,"p99":306.6141612308178}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:27+00","p50":171.64337225,"p95":314.9374033931488,"p99":397.1171208631456}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:28+00","p50":158.3884395,"p95":304.8563426973292,"p99":339.7822106557975}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:29+00","p50":92.627467,"p95":202.0216888137951,"p99":280.90554138609605}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:30+00","p50":96.61781825,"p95":157.9654235613621,"p99":186.83731457728626}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:31+00","p50":123.43314362500001,"p95":199.3941166258743,"p99":245.45484031698942}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:32+00","p50":124.135185,"p95":220.18840280805207,"p99":307.5864315479431}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:33+00","p50":140.19246900000002,"p95":229.95685554513193,"p99":284.8045222781925}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:34+00","p50":170.89651500000002,"p95":296.9922443745279,"p99":373.8279744532552}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:35+00","p50":153.95292999999998,"p95":273.0253852019741,"p99":321.88157072990987}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:36+00","p50":116.791720375,"p95":239.27738784291387,"p99":300.4744579807956}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:37+00","p50":127.94282950000002,"p95":236.97901388224858,"p99":327.179691118208}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:38+00","p50":105.85115675,"p95":186.5823645810992,"p99":274.30453295752267}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:39+00","p50":96.74238075,"p95":175.41507650993736,"p99":208.9679198589525}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:40+00","p50":97.870805625,"p95":172.275364745659,"p99":201.6971528060169}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:41+00","p50":145.6703235,"p95":254.6114526171267,"p99":296.82222953919984}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:42+00","p50":193.5103525,"p95":336.3785704780355,"p99":387.57975299882554}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:43+00","p50":220.64773574999998,"p95":403.4065592694748,"p99":449.6418732563763}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:44+00","p50":119.84416999999999,"p95":295.6309464197681,"p99":396.337261710165}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:45+00","p50":153.03005225,"p95":273.1666237380255,"p99":345.72557337760685}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:46+00","p50":127.57828725,"p95":256.893004685591,"p99":289.8162797670803}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:47+00","p50":111.580409625,"p95":203.82922388112414,"p99":261.35195212518715}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:48+00","p50":216.393405,"p95":375.25428619803426,"p99":453.76364603817603}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:49+00","p50":152.94896525,"p95":283.33596944424914,"p99":336.73562894160085}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:50+00","p50":127.08926425,"p95":221.00235614622642,"p99":262.39391229211236}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:51+00","p50":120.027496,"p95":250.44809182697068,"p99":296.46660237570285}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:52+00","p50":208.19365549999998,"p95":333.84489628812776,"p99":385.3186423182583}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:53+00","p50":133.24839425,"p95":289.755634198976,"p99":336.5386512031822}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:54+00","p50":103.00979025000001,"p95":185.00440500332434,"p99":233.78304482841588}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:55+00","p50":119.260761375,"p95":302.3292177795595,"p99":339.6823966917932}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:56+00","p50":171.5847955,"p95":283.52003666019885,"p99":338.9246723470902}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:57+00","p50":210.90060975,"p95":357.92389919369606,"p99":451.1805926516762}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:58+00","p50":197.74463350000002,"p95":420.3640920281507,"p99":475.54871186478806}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:29:59+00","p50":116.13341187500001,"p95":216.05566825794946,"p99":258.6462742256367}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:00+00","p50":150.502252,"p95":236.99798954159232,"p99":277.9683100395851}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:01+00","p50":129.45309675,"p95":243.01425622196734,"p99":263.2717654624682}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:02+00","p50":140.40493125,"p95":244.6623756164837,"p99":293.2006867225514}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:03+00","p50":140.72530325000002,"p95":266.3681622788145,"p99":314.5436941961575}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:04+00","p50":117.9053805,"p95":236.25603219293595,"p99":291.05249448661044}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:05+00","p50":98.568882,"p95":188.8854632627058,"p99":262.0676099577932}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:06+00","p50":132.206959375,"p95":218.22767605911855,"p99":254.24492835280157}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:07+00","p50":152.504794625,"p95":260.0884505841782,"p99":314.5586549095628}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:08+00","p50":124.92891800000001,"p95":226.26183568808176,"p99":261.7090193795609}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:09+00","p50":163.63329149999998,"p95":311.59877173680616,"p99":394.78992070520303}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:10+00","p50":208.2068475,"p95":368.13115528785085,"p99":419.1755257724094}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:11+00","p50":123.78249262499999,"p95":296.04961330785375,"p99":338.4488425894005}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:12+00","p50":95.495242125,"p95":148.29508863492148,"p99":177.30971520834305}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:13+00","p50":107.90742599999999,"p95":190.8493691335392,"p99":242.4437651493988}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:14+00","p50":112.8262655,"p95":189.02962614134216,"p99":233.83208174513533}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:15+00","p50":129.43901200000002,"p95":222.55574918017626,"p99":265.85857962914633}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:16+00","p50":111.507611,"p95":251.323296742464,"p99":326.25064225217744}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:17+00","p50":145.8751935,"p95":261.9316979401115,"p99":287.29736789018676}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:18+00","p50":140.98935925,"p95":219.09282268333135,"p99":252.7970128424511}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:19+00","p50":103.66248149999998,"p95":196.7206822273125,"p99":245.7875742989993}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:20+00","p50":108.628296,"p95":214.60698472325123,"p99":269.06154806882523}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:21+00","p50":136.149549,"p95":255.86972806917476,"p99":332.67014772443395}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:22+00","p50":197.93880687499998,"p95":381.502435144323,"p99":588.6820270814882}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:23+00","p50":155.857772,"p95":267.31127404588386,"p99":341.7845688213129}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:24+00","p50":121.2367075,"p95":230.69728676805232,"p99":309.2485743088274}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:25+00","p50":115.092376625,"p95":217.68924514860404,"p99":295.46614822230623}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:26+00","p50":104.975132375,"p95":196.17456882825775,"p99":270.3656087240508}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:27+00","p50":109.05953600000001,"p95":212.36383694926678,"p99":264.33129210214855}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:28+00","p50":71.796504,"p95":134.5068019675878,"p99":147.5120745979662}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:29+00","p50":106.43898575,"p95":194.79783965432463,"p99":225.63208004748202}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:30+00","p50":137.46628987499997,"p95":225.69145738857168,"p99":292.93187633087496}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:31+00","p50":214.83264099999997,"p95":346.9228359460459,"p99":415.6551393588102}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:32+00","p50":133.481192625,"p95":301.53098885368274,"p99":474.0881293052933}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:33+00","p50":132.69280350000002,"p95":234.10646878798485,"p99":263.2966781649246}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:34+00","p50":106.864666,"p95":174.25787302286147,"p99":205.7175736684494}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:35+00","p50":103.110114875,"p95":177.08113088051854,"p99":192.95483759117724}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:36+00","p50":128.914074,"p95":243.58446722036265,"p99":271.0209191268463}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:37+00","p50":122.67461,"p95":245.05329255421358,"p99":333.7442105880328}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:38+00","p50":100.9746515,"p95":173.47282376597977,"p99":221.09815979568864}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:39+00","p50":154.003629875,"p95":287.3173631577436,"p99":369.43251293130015}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:40+00","p50":146.8971305,"p95":251.92130827648484,"p99":291.40066258055305}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:41+00","p50":151.8076725,"p95":282.6784084142944,"p99":328.939611905571}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:42+00","p50":149.10895899999997,"p95":256.15891355442113,"p99":316.8641798551555}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:43+00","p50":188.96112225,"p95":353.2331218520702,"p99":382.0589271104278}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:44+00","p50":105.42887637499999,"p95":195.13140718747843,"p99":224.0785774883292}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:45+00","p50":101.91120699999999,"p95":166.9423531193254,"p99":222.36277018176125}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:46+00","p50":121.87638712500001,"p95":220.37070360914117,"p99":276.06403395931005}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:47+00","p50":108.74720350000001,"p95":219.51700998232008,"p99":275.2252684501667}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:48+00","p50":93.153455,"p95":151.74555846118363,"p99":181.83308822772503}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:49+00","p50":112.29081587499999,"p95":204.4243987085814,"p99":237.5612214038899}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:50+00","p50":148.8138005,"p95":248.82397409293438,"p99":289.9684980177193}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:51+00","p50":149.66555962500001,"p95":297.56756080056897,"p99":350.7251047181592}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:52+00","p50":145.37337025,"p95":230.90831475004887,"p99":253.95216025196981}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:53+00","p50":117.05370775,"p95":210.9270279865246,"p99":230.78916404208755}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:54+00","p50":87.1349905,"p95":199.58254975250549,"p99":269.7982071014629}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:55+00","p50":84.445145375,"p95":174.8095905966679,"p99":224.31852516622257}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:56+00","p50":162.824442,"p95":290.1118759925517,"p99":358.80143425187447}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:57+00","p50":164.86294875,"p95":277.7016547873936,"p99":326.2230259840393}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:58+00","p50":175.636123,"p95":287.96937075817937,"p99":355.6185225502338}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:30:59+00","p50":150.038473875,"p95":252.2978227349552,"p99":300.730214680809}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:00+00","p50":128.66758900000002,"p95":214.68781069859946,"p99":286.44018709812354}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:01+00","p50":128.1123915,"p95":222.11258651795723,"p99":264.97585152214816}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:02+00","p50":105.31138775000001,"p95":172.40821545982874,"p99":200.62516225891375}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:03+00","p50":130.089294125,"p95":212.50929469829094,"p99":334.07392903572133}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:04+00","p50":127.4692995,"p95":210.03156893045616,"p99":267.3158943193741}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:05+00","p50":124.12707212499998,"p95":206.27348324430156,"p99":273.6812023887224}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:06+00","p50":114.39556999999999,"p95":216.97375798055268,"p99":264.0877355136185}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:07+00","p50":99.448616375,"p95":184.96942576062798,"p99":205.90453483523345}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:08+00","p50":171.676611375,"p95":291.3521424865109,"p99":380.8211314464147}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:09+00","p50":101.6901615,"p95":176.22343908884048,"p99":201.53023230511474}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:10+00","p50":103.82912900000001,"p95":191.72203332268143,"p99":212.09726031407547}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:11+00","p50":165.89840875,"p95":332.89048460847096,"p99":367.4451517877708}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:12+00","p50":119.054623375,"p95":271.49086032681515,"p99":370.2199425715115}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:13+00","p50":112.88282125,"p95":209.73930696244759,"p99":261.1041244435084}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:14+00","p50":126.00579475,"p95":188.01621412764,"p99":247.65884071391585}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:15+00","p50":124.52331475,"p95":317.90755742074407,"p99":406.3813968398581}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:16+00","p50":109.03048712500001,"p95":196.21054002123554,"p99":238.74807661986733}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:17+00","p50":90.9006375,"p95":139.70213585222933,"p99":178.89033774899198}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:18+00","p50":133.20292899999998,"p95":252.13865144739617,"p99":334.3060690541854}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:19+00","p50":182.47337875,"p95":388.51738978338915,"p99":481.33885515605544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:20+00","p50":200.02103775,"p95":380.6488631596406,"p99":505.64067757334544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:21+00","p50":142.2534555,"p95":244.77796356592177,"p99":305.4332737325897}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:22+00","p50":93.26102975,"p95":183.74352975703098,"p99":221.36483379478076}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:23+00","p50":96.7112195,"p95":170.64173087076998,"p99":205.3115922099886}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:24+00","p50":182.41513,"p95":304.1554060275163,"p99":470.23418123118444}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:25+00","p50":107.133741,"p95":283.87971806412753,"p99":333.2628895482388}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:26+00","p50":146.30091775,"p95":298.11623706274673,"p99":346.4917418987236}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:27+00","p50":94.27910700000001,"p95":174.53994674142575,"p99":228.0240196648221}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:28+00","p50":116.094989,"p95":206.6467516277365,"p99":245.66344650732017}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:29+00","p50":72.352633875,"p95":127.79524663746197,"p99":144.08114570358322}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:30+00","p50":105.882624875,"p95":185.42919072380667,"p99":240.22683089604402}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:31+00","p50":132.1897915,"p95":234.17223494993186,"p99":279.2885025061626}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:32+00","p50":105.253751,"p95":180.89204505133415,"p99":257.22610878321314}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:33+00","p50":85.311459125,"p95":174.77236335982323,"p99":206.10641941095756}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:34+00","p50":131.713477625,"p95":239.26062745615036,"p99":275.05193150050525}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:35+00","p50":78.08752312499999,"p95":143.3597636767404,"p99":195.5654004745698}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:36+00","p50":141.59339425,"p95":237.79552032503491,"p99":292.4438382544732}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:37+00","p50":116.728553,"p95":235.73840350087522,"p99":288.31137631500053}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:38+00","p50":161.1288965,"p95":286.2833288930545,"p99":325.9958004062338}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:39+00","p50":142.132838,"p95":298.41372608770445,"p99":383.3481538692069}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:40+00","p50":82.19294775,"p95":259.5134540716391,"p99":329.1845178000543}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:41+00","p50":192.369893875,"p95":342.4390612479822,"p99":437.8914614013498}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:42+00","p50":142.40547637500003,"p95":249.99869662591362,"p99":276.40473861436675}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:43+00","p50":115.655135125,"p95":229.15395159425157,"p99":255.3132673618655}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:44+00","p50":98.39828524999999,"p95":180.54260905758446,"p99":230.59197629774192}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:45+00","p50":83.187268,"p95":143.84027024919862,"p99":197.0350250535693}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:46+00","p50":95.00053199999999,"p95":168.239280681592,"p99":190.79333302597047}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:47+00","p50":105.25111125000001,"p95":169.702421905602,"p99":181.13400749610423}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:48+00","p50":100.675961,"p95":180.47359538495363,"p99":225.03751616834424}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:49+00","p50":149.31501775,"p95":254.13259542648188,"p99":303.69875910796475}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:50+00","p50":131.956781,"p95":238.34048604075312,"p99":288.58045255291273}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:51+00","p50":137.081439625,"p95":242.76159943214202,"p99":348.1319630319319}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:52+00","p50":128.550900625,"p95":304.49830668660553,"p99":418.9931111537857}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:53+00","p50":113.432842875,"p95":207.30350411016087,"p99":272.14124509863547}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:54+00","p50":92.045899625,"p95":157.18261061900532,"p99":183.07132557156112}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:55+00","p50":121.734187125,"p95":207.5546754412605,"p99":246.67373636486053}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:56+00","p50":140.65304175,"p95":232.28193527760607,"p99":286.8730073162615}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:57+00","p50":137.850933,"p95":228.62296795999146,"p99":260.72006399337766}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:58+00","p50":112.27431712499998,"p95":220.46518146997607,"p99":260.3620207610254}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:31:59+00","p50":82.67948799999999,"p95":360.23146170482016,"p99":398.9918931486969}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:00+00","p50":86.90940225,"p95":158.23311998827646,"p99":200.37656756777955}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:01+00","p50":123.76849200000001,"p95":208.02569298310027,"p99":236.88757799383976}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:02+00","p50":111.959362625,"p95":191.38780734815316,"p99":215.81521757097363}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:03+00","p50":145.8964775,"p95":223.0613097268436,"p99":241.46586528538083}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:04+00","p50":107.172940875,"p95":193.5446881791165,"p99":272.19900877614333}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:05+00","p50":151.58030200000002,"p95":271.3545077958731,"p99":341.0796747550945}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:06+00","p50":124.9742315,"p95":247.65959759302604,"p99":312.5631921222915}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:07+00","p50":161.23117825,"p95":299.3136786651933,"p99":395.0948306106245}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:08+00","p50":101.39191525,"p95":173.38602668127538,"p99":194.33212311404063}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:09+00","p50":108.5149455,"p95":189.76618764198923,"p99":257.21958497325323}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:10+00","p50":186.05221650000001,"p95":303.3886631053319,"p99":390.5604375602221}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:11+00","p50":192.94315225,"p95":351.44012321469,"p99":395.4634256011743}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:12+00","p50":134.618117,"p95":231.5193473509258,"p99":285.5411902005575}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:13+00","p50":128.685649,"p95":214.19918122306908,"p99":250.87769240809203}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:14+00","p50":76.36108625,"p95":148.526741802287,"p99":181.20134408114671}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:15+00","p50":67.19718524999999,"p95":125.55531013336198,"p99":154.58911684265348}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:16+00","p50":114.42459025,"p95":197.34831910622728,"p99":224.35313192522668}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:17+00","p50":97.26033925,"p95":186.9463952725586,"p99":226.53538178850508}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:18+00","p50":154.234702,"p95":257.78904323379743,"p99":309.2634639996348}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:19+00","p50":89.865234875,"p95":194.70757852958704,"p99":254.24742956245254}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:20+00","p50":141.04095812499997,"p95":248.75033183114076,"p99":287.16187705317617}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:21+00","p50":132.46721250000002,"p95":269.21060446316676,"p99":322.2473977469425}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:22+00","p50":121.54987225,"p95":219.82430763507486,"p99":266.94648113744637}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:23+00","p50":109.96988325000001,"p95":210.29696443055713,"p99":257.5379662758779}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:24+00","p50":128.87029374999997,"p95":223.84370127555226,"p99":268.6310206921792}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:25+00","p50":132.44363125,"p95":232.90757436558246,"p99":259.72644852773476}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:26+00","p50":116.18927637499999,"p95":199.00330712602562,"p99":234.83804375148225}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:27+00","p50":85.932075,"p95":162.45912678770637,"p99":192.2755196891861}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:28+00","p50":123.77242075,"p95":238.85725559116884,"p99":320.52498529224204}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:29+00","p50":132.682416,"p95":239.48273816366486,"p99":292.2031849994033}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:30+00","p50":98.13990824999999,"p95":208.4430295585929,"p99":297.3711427686527}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:31+00","p50":81.74755675,"p95":144.36437531812751,"p99":184.41029792012452}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:32+00","p50":109.766641875,"p95":186.01520823519493,"p99":253.3463953382125}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:33+00","p50":111.24209074999999,"p95":188.48749389847183,"p99":223.45380387124635}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:34+00","p50":73.9090515,"p95":128.46959014384055,"p99":160.96053139662362}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:35+00","p50":146.60734250000002,"p95":276.5788107211896,"p99":326.3968412678018}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:36+00","p50":177.80567675,"p95":325.34464427588046,"p99":342.38701714505817}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:37+00","p50":133.549765875,"p95":224.52639736927665,"p99":260.706587498024}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:38+00","p50":135.187723375,"p95":243.5794123138162,"p99":273.6018945712824}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:39+00","p50":103.708696,"p95":193.05857363887185,"p99":227.22884904382968}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:40+00","p50":168.0875605,"p95":288.32557100474133,"p99":429.4105909170537}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:41+00","p50":113.044901,"p95":204.66593179067422,"p99":248.80736430322028}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:42+00","p50":106.138461,"p95":199.81239802513886,"p99":247.1777182748711}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:43+00","p50":107.3592585,"p95":192.49416815036332,"p99":224.8814767806611}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:44+00","p50":99.18897625,"p95":174.70940486153,"p99":237.11470528038262}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:45+00","p50":95.84890150000001,"p95":161.41773575798928,"p99":219.890401485641}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:46+00","p50":103.0340435,"p95":191.71927127210122,"p99":231.2024288434174}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:47+00","p50":120.54642625000001,"p95":216.0881204954477,"p99":256.4036516719635}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:48+00","p50":166.69941799999998,"p95":268.5081619204737,"p99":318.18447506379647}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:49+00","p50":192.62847900000003,"p95":324.60369583223536,"p99":461.1589813419571}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:50+00","p50":131.20099274999998,"p95":261.35673138289144,"p99":302.75657998520495}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:51+00","p50":123.651792125,"p95":210.23242448413325,"p99":246.3328844222181}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:52+00","p50":131.9947435,"p95":231.40216184965848,"p99":285.17395579256873}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:53+00","p50":78.83826049999999,"p95":148.90721440765452,"p99":173.81949700158404}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:54+00","p50":125.987029,"p95":264.2961238741703,"p99":355.7167631486969}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:55+00","p50":91.7448945,"p95":175.53598506476308,"p99":255.90322352300262}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:56+00","p50":159.301579,"p95":351.84188328943253,"p99":389.8347058511143}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:57+00","p50":187.98843125000002,"p95":340.9330024263749,"p99":475.7749451908893}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:58+00","p50":93.300342,"p95":176.6299474837494,"p99":204.57259773898315}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:32:59+00","p50":154.010657,"p95":268.27781002858546,"p99":351.9433132745383}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:00+00","p50":89.591733375,"p95":197.85621017018903,"p99":286.63712836794735}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:01+00","p50":144.52407725,"p95":260.72149573640036,"p99":350.88902211560537}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:02+00","p50":167.78390574999997,"p95":261.1920397860639,"p99":308.44339777837183}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:03+00","p50":110.457206375,"p95":232.94494782296835,"p99":310.952956860805}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:04+00","p50":130.58692025,"p95":232.94350894737363,"p99":262.5543511986141}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:05+00","p50":132.6726885,"p95":274.8975297670839,"p99":305.89513347542663}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:06+00","p50":138.21469925000002,"p95":229.24087746859925,"p99":272.87511199417753}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:07+00","p50":121.148952125,"p95":206.83780719944411,"p99":280.29341221235273}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:08+00","p50":136.28463349999998,"p95":233.30969393610476,"p99":262.1581118301449}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:09+00","p50":139.2178705,"p95":262.7049547678597,"p99":306.15073195677854}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:10+00","p50":93.3006045,"p95":158.67369987372612,"p99":211.49455500391838}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:11+00","p50":121.19626,"p95":215.20547866883504,"p99":239.91287958024455}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:12+00","p50":117.82031975000001,"p95":243.7224320210201,"p99":298.88462132959245}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:13+00","p50":97.39294425,"p95":193.14224585835888,"p99":274.1118376800118}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:14+00","p50":126.019774125,"p95":216.64769596165985,"p99":247.57431191786193}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:15+00","p50":110.28737849999999,"p95":207.9735082817216,"p99":258.7438023121443}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:16+00","p50":151.769137,"p95":271.2666339292557,"p99":324.4723331002159}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:17+00","p50":108.65530050000001,"p95":248.16915056245446,"p99":276.4589559216995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:18+00","p50":102.02753899999999,"p95":190.30595644983174,"p99":216.97046380724905}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:19+00","p50":171.95970400000004,"p95":325.80248962108226,"p99":391.3254006926124}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:20+00","p50":129.030809125,"p95":234.70576515157012,"p99":333.13880387933284}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:21+00","p50":142.003728625,"p95":259.38421134843156,"p99":276.98313699413274}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:22+00","p50":135.99274687500002,"p95":231.98935542941453,"p99":266.1397871506088}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:23+00","p50":130.761799,"p95":239.40395889858198,"p99":275.6459304371319}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:24+00","p50":160.697157625,"p95":308.61707404364756,"p99":381.15502791236713}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:25+00","p50":158.024736625,"p95":278.77260017359885,"p99":328.49168184143736}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:26+00","p50":114.71008275,"p95":218.13452320170498,"p99":272.1544344899063}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:27+00","p50":140.559158,"p95":249.47429859301414,"p99":326.64463440939664}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:28+00","p50":111.771714125,"p95":207.278551033642,"p99":245.6814544792559}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:29+00","p50":114.24286012499999,"p95":200.80320238073097,"p99":254.4697853079252}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:30+00","p50":135.1836505,"p95":217.6982606517795,"p99":254.974771001374}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:31+00","p50":153.135532875,"p95":277.50303641609884,"p99":355.2305931752739}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:32+00","p50":136.95915825,"p95":224.5368081892381,"p99":267.11966566760253}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:33+00","p50":132.29825575,"p95":239.57585468849206,"p99":314.60056932971}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:34+00","p50":103.399906,"p95":172.11769942860485,"p99":213.15400028454636}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:35+00","p50":175.490225,"p95":284.89367610841134,"p99":315.35000088033013}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:36+00","p50":109.953139,"p95":219.09903000609307,"p99":301.08569529499914}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:37+00","p50":165.88423212499998,"p95":260.63896857591743,"p99":313.8628484955995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:38+00","p50":101.93700100000001,"p95":213.01136650430917,"p99":245.1802027982273}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:39+00","p50":108.51960975,"p95":202.73559789456934,"p99":277.46021133806397}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:40+00","p50":142.54735749999998,"p95":228.79483657287597,"p99":246.43256146665954}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:41+00","p50":157.663866375,"p95":282.3046626167348,"p99":371.38470887483095}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:42+00","p50":103.76186637500001,"p95":212.9482790310916,"p99":244.19946417914989}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:43+00","p50":137.08775550000001,"p95":218.11688260544997,"p99":256.1722065202479}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:44+00","p50":87.19501025,"p95":170.508156930151,"p99":196.0298240378456}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:45+00","p50":105.60156825,"p95":179.86273027825283,"p99":242.1840284773283}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:46+00","p50":126.435669,"p95":221.10153320637858,"p99":274.56351550732353}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:47+00","p50":160.789563625,"p95":266.25205245555117,"p99":304.50109863255807}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:48+00","p50":194.456216875,"p95":339.3783954876421,"p99":397.76490635657404}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:49+00","p50":171.189518,"p95":363.8666234660058,"p99":391.60687616126177}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:50+00","p50":119.11517475,"p95":216.62137560765362,"p99":274.90732011648754}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:51+00","p50":172.479413,"p95":371.08197138914727,"p99":436.8373595098934}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:52+00","p50":209.34127925,"p95":412.8800077645817,"p99":524.2879604627418}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:53+00","p50":108.457824375,"p95":194.35176210737097,"p99":260.60692312551595}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:54+00","p50":96.7310435,"p95":194.81557172022556,"p99":217.31256000707768}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:55+00","p50":93.200077,"p95":165.1387342724762,"p99":238.9854731506958}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:56+00","p50":142.599244625,"p95":237.38065401918053,"p99":314.6896877325849}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:57+00","p50":121.9096795,"p95":215.98683936081864,"p99":268.30810155489877}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:58+00","p50":154.69943625,"p95":280.98262648235084,"p99":341.8890496463246}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:33:59+00","p50":199.37045137500002,"p95":343.2554753140477,"p99":389.8407251554308}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:00+00","p50":178.971035,"p95":357.6685898514217,"p99":383.0474701989002}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:01+00","p50":108.24118,"p95":211.56502163815497,"p99":253.66431989964295}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:02+00","p50":112.951525375,"p95":209.58080252835285,"p99":257.68494610107945}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:03+00","p50":126.61733275,"p95":270.69568282047254,"p99":297.6661016650088}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:04+00","p50":129.74679237499998,"p95":207.89669193858487,"p99":234.7650246639769}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:05+00","p50":113.205066625,"p95":222.64535708457143,"p99":286.5436205269561}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:06+00","p50":158.845006,"p95":272.6981570798247,"p99":311.22377243093683}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:07+00","p50":104.269959375,"p95":186.26893955622936,"p99":257.13570341709277}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:08+00","p50":82.7810755,"p95":158.64511222286438,"p99":181.98047218651055}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:09+00","p50":130.39636025,"p95":264.983896109309,"p99":321.7425447593372}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:10+00","p50":181.28793312499997,"p95":342.4388419570696,"p99":419.3970190465395}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:11+00","p50":108.2284715,"p95":211.1396561140374,"p99":287.32048669470544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:12+00","p50":90.75315950000001,"p95":147.6862662737088,"p99":183.43829499481583}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:13+00","p50":129.70711475,"p95":225.37225429627068,"p99":255.92971481642866}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:14+00","p50":96.686889125,"p95":201.59629984932673,"p99":231.71299279920603}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:15+00","p50":156.83113012500002,"p95":253.60921267468444,"p99":302.6166148413315}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:16+00","p50":91.41997925,"p95":222.71644634844162,"p99":250.9376056548529}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:17+00","p50":90.17032687499999,"p95":183.64460838993273,"p99":207.08017901870681}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:18+00","p50":167.765525125,"p95":280.3460425070659,"p99":363.823567006599}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:19+00","p50":100.856831375,"p95":234.378799980344,"p99":364.5741734524064}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:20+00","p50":96.920702,"p95":186.43524481054914,"p99":210.78910242640828}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:21+00","p50":146.608061,"p95":263.1426511173795,"p99":366.08512019191}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:22+00","p50":129.79841375,"p95":234.75451710481312,"p99":279.89254948362634}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:23+00","p50":113.31917762500001,"p95":204.2847229782487,"p99":230.81283798620152}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:24+00","p50":95.22748787500001,"p95":179.70511902539852,"p99":211.31660937055995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:25+00","p50":131.932868875,"p95":246.5846490138291,"p99":296.77279765726877}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:26+00","p50":134.8642885,"p95":237.88675587452542,"p99":315.2661447682567}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:27+00","p50":113.56584537500001,"p95":211.6464750063566,"p99":258.8126012360246}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:28+00","p50":96.39151675,"p95":204.11704643050504,"p99":234.51858652154112}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:29+00","p50":85.31185950000001,"p95":149.92995711764468,"p99":175.2174036983316}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:30+00","p50":104.6222635,"p95":234.7705419496284,"p99":275.8478079417207}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:31+00","p50":171.117827875,"p95":272.7976127010844,"p99":303.0287115965159}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:32+00","p50":168.08565525,"p95":255.63354273508656,"p99":310.89784066886284}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:33+00","p50":125.60047025,"p95":255.60529882844438,"p99":371.2388122660852}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:34+00","p50":80.927663,"p95":180.48101032611038,"p99":213.6153891947932}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:35+00","p50":111.1060105,"p95":194.52131252360152,"p99":246.75163256098173}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:36+00","p50":127.591422375,"p95":287.72788504626834,"p99":304.1742515809479}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:37+00","p50":111.77776,"p95":222.28883277152394,"p99":246.4496057113104}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:38+00","p50":123.21690575,"p95":203.3015751411889,"p99":224.77546342928912}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:39+00","p50":61.298322999999996,"p95":130.8376594201172,"p99":158.07295036492326}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:40+00","p50":109.12991550000001,"p95":198.45098782836126,"p99":240.51228300533674}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:41+00","p50":119.636812125,"p95":223.49270004305305,"p99":271.2792305754044}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:42+00","p50":136.165448125,"p95":269.09324422772914,"p99":329.6001543707435}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:43+00","p50":83.66977875,"p95":175.86070786282778,"p99":206.28558013973262}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:44+00","p50":141.644276375,"p95":276.3125038332239,"p99":2714.0188136444194}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:45+00","p50":92.90529825,"p95":400.0358297394864,"p99":2680.65359600007}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:46+00","p50":103.66558599999999,"p95":200.01841377174986,"p99":230.39338538344907}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:47+00","p50":112.409372625,"p95":193.13745166308456,"p99":237.46652794846202}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:48+00","p50":73.83905100000001,"p95":126.76489645875716,"p99":167.26468065236187}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:49+00","p50":87.221907,"p95":145.52034115621473,"p99":180.85200492792322}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:50+00","p50":130.782604,"p95":219.41988529901056,"p99":285.541460149041}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:51+00","p50":195.28480574999998,"p95":326.0018023254571,"p99":373.34627889115905}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:52+00","p50":106.6144605,"p95":169.45471065255182,"p99":243.70530942599464}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:53+00","p50":93.39250650000001,"p95":156.9634457514476,"p99":178.2786364682007}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:54+00","p50":90.598717,"p95":148.46863580962133,"p99":186.4363788593211}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:55+00","p50":118.172082375,"p95":208.0808623812211,"p99":258.67128908991475}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:56+00","p50":83.566433,"p95":194.41415692042833,"p99":231.49819592396497}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:57+00","p50":105.006986,"p95":202.96383503430675,"p99":234.14788155763148}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:58+00","p50":162.067145875,"p95":279.4822295971695,"p99":310.82483102614924}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:34:59+00","p50":169.45675,"p95":329.4382836908407,"p99":345.91755786689424}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:00+00","p50":166.500713875,"p95":279.0223933728053,"p99":294.4110626556824}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:01+00","p50":107.515719375,"p95":190.58503886516323,"p99":284.9832490318682}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:02+00","p50":118.99001924999999,"p95":208.83248951508372,"p99":224.4868183647139}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:03+00","p50":153.991842875,"p95":331.00365691033943,"p99":398.79504665015696}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:04+00","p50":121.6693395,"p95":211.38797440596198,"p99":247.40738793343544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:05+00","p50":111.77233412500001,"p95":199.6812506939835,"p99":223.45671930504415}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:06+00","p50":82.6729975,"p95":150.67689882769298,"p99":175.77364763083457}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:07+00","p50":107.557526,"p95":201.11227422222532,"p99":231.84727361766434}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:08+00","p50":66.266209,"p95":141.25337486676025,"p99":193.79314652908133}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:09+00","p50":164.94543575,"p95":330.9845541875801,"p99":386.4475589318237}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:10+00","p50":183.5891835,"p95":301.78170202836503,"p99":339.57459430127307}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:11+00","p50":109.875771375,"p95":229.88328048201143,"p99":314.20519106488535}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:12+00","p50":90.54347974999999,"p95":162.44295664938545,"p99":174.23847641368008}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:13+00","p50":125.342369125,"p95":211.7711552228204,"p99":292.7087673110201}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:14+00","p50":139.79976999999997,"p95":237.32373536660933,"p99":303.4027288497977}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:15+00","p50":111.83360462499999,"p95":179.0283804484157,"p99":289.8403239086296}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:16+00","p50":106.7065,"p95":189.64294040120888,"p99":213.0033925261383}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:17+00","p50":98.95059874999998,"p95":182.04198939775986,"p99":219.79354853570342}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:18+00","p50":74.783671,"p95":139.38796541724872,"p99":175.566532658432}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:19+00","p50":130.7210235,"p95":221.81972287425995,"p99":240.96419853359032}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:20+00","p50":145.00668724999997,"p95":261.9174239511622,"p99":286.089030662014}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:21+00","p50":153.838682,"p95":272.5404138289604,"p99":323.2369829031449}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:22+00","p50":122.223364625,"p95":238.23110339961977,"p99":368.38718381865243}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:23+00","p50":162.310989625,"p95":262.4874787790148,"p99":293.1220457818482}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:24+00","p50":125.672744,"p95":228.834117063434,"p99":300.69801505506183}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:25+00","p50":100.214716,"p95":211.75717983390211,"p99":272.17507607588095}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:26+00","p50":101.04452025,"p95":245.69881332390523,"p99":331.91489407186367}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:27+00","p50":111.960121,"p95":190.03785640707778,"p99":217.72857811685657}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:28+00","p50":97.04044925,"p95":221.86339509644938,"p99":305.54954175209235}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:29+00","p50":130.712100625,"p95":229.55412405702597,"p99":320.45543533930805}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:30+00","p50":157.77975600000002,"p95":270.5441781738374,"p99":328.65896650134135}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:31+00","p50":196.70007750000002,"p95":348.0615820548358,"p99":426.0200593051596}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:32+00","p50":192.89058437499997,"p95":318.9880148114973,"p99":368.06064510008764}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:33+00","p50":185.264564,"p95":365.25417587183165,"p99":432.176320351829}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:34+00","p50":133.7000965,"p95":284.4834116264355,"p99":345.56122470449804}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:35+00","p50":84.80136974999999,"p95":159.84577253030898,"p99":193.07529147513296}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:36+00","p50":130.104786,"p95":215.77668530763603,"p99":244.13811818882513}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:37+00","p50":150.7244015,"p95":277.5214597349701,"p99":314.3206531936665}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:38+00","p50":192.87258300000002,"p95":294.449891230029,"p99":338.93015122969626}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:39+00","p50":165.072821375,"p95":311.3453248371123,"p99":426.97387214368536}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:40+00","p50":154.464597,"p95":295.15971093502424,"p99":332.91260871096466}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:41+00","p50":135.8931455,"p95":233.624761005939,"p99":260.5115031151123}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:42+00","p50":135.18295024999998,"p95":244.3717553656875,"p99":300.2150756749728}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:43+00","p50":119.91235475,"p95":224.3114507049048,"p99":307.1051734783702}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:44+00","p50":137.148140375,"p95":259.6874207851501,"p99":289.1676174127986}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:45+00","p50":147.97824462499997,"p95":260.3007213891845,"p99":324.2455354255281}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:46+00","p50":147.1479335,"p95":241.8578711938247,"p99":280.00141094697403}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:47+00","p50":150.817367625,"p95":248.70500563243021,"p99":277.4436327296431}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:48+00","p50":144.171619125,"p95":251.37096323751678,"p99":298.17490467755624}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:49+00","p50":123.89109400000001,"p95":204.1136985168762,"p99":243.7539701242161}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:50+00","p50":122.72740062499999,"p95":193.95676649233684,"p99":241.66564591236522}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:51+00","p50":153.94507875,"p95":265.17707475493455,"p99":305.60247756110095}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:52+00","p50":144.988382,"p95":312.44177368438767,"p99":363.26797275389765}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:53+00","p50":109.40713274999999,"p95":197.46090179334794,"p99":240.15491899285365}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:54+00","p50":139.35138024999998,"p95":225.2924647808224,"p99":277.5620172876086}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:55+00","p50":113.9832725,"p95":186.40617246659897,"p99":215.05913817651629}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:56+00","p50":156.15925600000003,"p95":287.82816145174536,"p99":354.85521886894867}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:57+00","p50":115.40843,"p95":239.05164655578875,"p99":316.59567401850416}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:58+00","p50":197.7765975,"p95":416.1175502186935,"p99":466.469045107173}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:35:59+00","p50":202.89194087500002,"p95":349.63866622645145,"p99":404.56312846058034}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:00+00","p50":198.94459774999999,"p95":353.76308783153826,"p99":395.96712162325196}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:01+00","p50":168.26020775,"p95":338.2364929774536,"p99":431.77628162248186}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:02+00","p50":109.2120755,"p95":205.0478798957319,"p99":242.13469274016762}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:03+00","p50":111.39407274999999,"p95":225.32265645215057,"p99":260.76947847130964}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:04+00","p50":94.35447425,"p95":141.59536676037325,"p99":163.48037051701021}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:05+00","p50":85.93396899999999,"p95":164.1230775428853,"p99":200.39024120283173}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:06+00","p50":98.885102625,"p95":185.8311925714839,"p99":210.4537069738741}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:07+00","p50":135.672643,"p95":222.0119241550026,"p99":283.4617929186172}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:08+00","p50":113.2663525,"p95":214.0534211434422,"p99":291.64135837551856}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:09+00","p50":159.1084185,"p95":269.92863143183604,"p99":358.1646831907215}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:10+00","p50":124.89934487500001,"p95":272.9495371308676,"p99":305.1021259294107}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:11+00","p50":122.506887875,"p95":234.41378886583172,"p99":296.42496434015055}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:12+00","p50":97.03467649999999,"p95":186.96049977567483,"p99":214.87227854773332}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:13+00","p50":83.69016125,"p95":133.79155361695814,"p99":147.42286262998772}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:14+00","p50":130.5168435,"p95":244.42995428340376,"p99":287.8777712488985}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:15+00","p50":167.95122425,"p95":278.0970642771322,"p99":317.7929958541353}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:16+00","p50":129.245699,"p95":252.42383743934357,"p99":310.9987554197951}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:17+00","p50":78.2792805,"p95":172.27121964769685,"p99":206.29942634598444}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:18+00","p50":115.9353545,"p95":229.6513937435913,"p99":252.0692335944824}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:19+00","p50":119.738077,"p95":226.624743089056,"p99":284.0741550035629}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:20+00","p50":120.81787162500001,"p95":224.35831306344093,"p99":297.32047582475377}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:21+00","p50":85.363958,"p95":142.83517089303862,"p99":171.71341252891827}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:22+00","p50":134.00057525,"p95":216.843783870425,"p99":268.0313999511449}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:23+00","p50":98.6495615,"p95":182.48137120181943,"p99":262.2612174592514}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:24+00","p50":86.41287137500001,"p95":159.28913771681565,"p99":217.95837422172664}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:25+00","p50":134.01032049999998,"p95":245.95001014836467,"p99":361.8789543562465}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:26+00","p50":181.01417225,"p95":288.14156706132604,"p99":319.9939622148242}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:27+00","p50":135.16686425,"p95":307.87106987401677,"p99":383.53602118434645}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:28+00","p50":142.205300625,"p95":313.0249734783502,"p99":377.02644868763946}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:29+00","p50":69.2453515,"p95":121.54252194107394,"p99":151.53891622078373}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:30+00","p50":108.191655,"p95":168.83107644189835,"p99":181.91957352425385}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:31+00","p50":91.42895200000001,"p95":163.39289189515924,"p99":193.32464757740473}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:32+00","p50":90.0382545,"p95":249.16111319746202,"p99":303.62188973125313}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:33+00","p50":122.363962875,"p95":241.11004143307892,"p99":275.80107516279816}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:34+00","p50":200.63016199999998,"p95":376.5515149010711,"p99":469.57317266088575}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:35+00","p50":122.65467775,"p95":222.1144769690965,"p99":246.10754225512196}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:36+00","p50":83.41168250000001,"p95":148.39955767729424,"p99":173.0133945137272}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:37+00","p50":91.794072,"p95":225.73530261131526,"p99":249.3132758702774}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:38+00","p50":154.7425085,"p95":250.6296928667202,"p99":312.40248126313116}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:39+00","p50":146.96510325,"p95":252.00715420247792,"p99":359.4976896411841}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:40+00","p50":111.192498,"p95":215.14796805219206,"p99":379.8821966365991}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:41+00","p50":158.5585935,"p95":292.3135766531309,"p99":320.2295283362978}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:42+00","p50":161.097306,"p95":372.4042890376837,"p99":422.32530508206486}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:43+00","p50":136.26324925,"p95":248.30755557928353,"p99":276.07229633098007}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:44+00","p50":132.17403175,"p95":314.0603097902488,"p99":360.81791175970574}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:45+00","p50":117.29861975,"p95":207.4477901520593,"p99":264.78193243250513}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:46+00","p50":127.744155,"p95":238.2121539305147,"p99":323.25874396489}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:47+00","p50":190.568953,"p95":321.52582331122613,"p99":403.9289674833584}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:48+00","p50":99.7234065,"p95":207.22368128198957,"p99":254.226341035182}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:49+00","p50":98.641797,"p95":170.39114444601012,"p99":209.21078709609222}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:50+00","p50":122.901254,"p95":236.46562268026614,"p99":349.6224602184286}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:51+00","p50":104.45667975,"p95":194.1477523933963,"p99":253.03547350740934}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:52+00","p50":114.95926725,"p95":219.06154841279363,"p99":251.95791725042534}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:53+00","p50":123.33415900000001,"p95":228.89528802697737,"p99":276.87857231985424}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:54+00","p50":179.009459,"p95":290.68070136162305,"p99":350.9395721945848}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:55+00","p50":120.13862175,"p95":235.68645373643875,"p99":286.35661302008435}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:56+00","p50":180.159685125,"p95":326.4572230714569,"p99":378.7087647571618}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:57+00","p50":235.578550375,"p95":380.6533190040628,"p99":414.50522828462243}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:58+00","p50":143.79758700000002,"p95":324.7384422801498,"p99":389.4072344903188}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:36:59+00","p50":119.07728924999998,"p95":223.149479398237,"p99":268.5456194129334}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:00+00","p50":133.12108475000002,"p95":219.15535152035147,"p99":251.53222348471667}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:01+00","p50":126.401039,"p95":242.26672297551443,"p99":279.5303177937584}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:02+00","p50":98.77711625,"p95":178.23476360726022,"p99":238.0362208637867}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:03+00","p50":117.43199075000001,"p95":192.11312790276992,"p99":212.01266422225808}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:04+00","p50":121.842190875,"p95":211.48700198306136,"p99":245.13858803551483}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:05+00","p50":128.44979425,"p95":222.07060683716495,"p99":249.12957749786904}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:06+00","p50":122.4439915,"p95":219.71387070132232,"p99":260.2719310353298}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:07+00","p50":105.01807099999999,"p95":192.89904914893864,"p99":228.09868004408077}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:08+00","p50":104.4691985,"p95":189.39088806279773,"p99":203.21527427568603}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:09+00","p50":96.351120125,"p95":150.88635408946882,"p99":177.30809496843958}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:10+00","p50":110.18914025000001,"p95":190.68050397718574,"p99":227.6492692492771}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:11+00","p50":155.3924105,"p95":257.6423159232403,"p99":286.6859072352004}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:12+00","p50":113.85714974999999,"p95":182.66569936267996,"p99":236.19490624672792}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:13+00","p50":150.649172875,"p95":258.9491673927281,"p99":317.4485613568304}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:14+00","p50":103.501193375,"p95":182.95739514124494,"p99":218.8111909035335}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:15+00","p50":125.908109,"p95":204.2527886215515,"p99":232.78885572620393}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:16+00","p50":130.657651875,"p95":206.25095609042953,"p99":233.61657293560742}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:17+00","p50":125.705275875,"p95":235.80634378492098,"p99":310.62447555340646}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:18+00","p50":191.381823,"p95":317.48981830126957,"p99":356.76570325650977}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:19+00","p50":77.895692,"p95":152.72282339270984,"p99":207.69731312943435}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:20+00","p50":149.289238,"p95":248.07458405307943,"p99":307.47401335983585}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:21+00","p50":162.321503,"p95":287.58862617272075,"p99":385.2868963209281}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:22+00","p50":94.476598,"p95":185.57725078226375,"p99":263.5700692414077}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:23+00","p50":126.8891835,"p95":212.52231472796785,"p99":282.6330093034568}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:24+00","p50":131.050623,"p95":245.01467374515806,"p99":304.28327870152236}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:25+00","p50":69.42449925,"p95":124.21130118484534,"p99":167.64549530899046}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:26+00","p50":141.440584125,"p95":292.85425696256783,"p99":363.6918490143297}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:27+00","p50":147.211423125,"p95":244.72929734618515,"p99":286.551358223305}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:28+00","p50":275.253966,"p95":474.39848426730356,"p99":546.3293572800286}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:29+00","p50":195.212622625,"p95":348.1170160236915,"p99":417.15105364123986}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:30+00","p50":131.156309875,"p95":218.6912256927759,"p99":237.44728682025098}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:31+00","p50":106.5761555,"p95":167.93664632811115,"p99":208.44386363696862}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:32+00","p50":116.456810875,"p95":194.59143006930142,"p99":231.62115326274417}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:33+00","p50":149.73172399999999,"p95":247.33003887072994,"p99":276.1448720705767}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:34+00","p50":175.44678075,"p95":284.36739915214383,"p99":380.7072540231259}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:35+00","p50":163.2328035,"p95":317.52868034062334,"p99":374.0800314037256}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:36+00","p50":116.24778125,"p95":211.65749028611637,"p99":235.97099018767813}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:37+00","p50":89.81987287500002,"p95":165.79688502524732,"p99":210.03703287565253}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:38+00","p50":97.795146,"p95":187.79941965014717,"p99":214.77224904475878}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:39+00","p50":175.165541625,"p95":291.1765228825621,"p99":339.39456672389935}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:40+00","p50":147.255911625,"p95":289.83857934598376,"p99":340.22948025997806}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:41+00","p50":131.28603850000002,"p95":237.17462068119428,"p99":295.76463757920646}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:42+00","p50":111.650405,"p95":180.8847583403759,"p99":207.64809259333802}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:43+00","p50":82.17003249999999,"p95":156.53934601291408,"p99":204.47241660359097}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:44+00","p50":96.872796,"p95":165.40618394574878,"p99":195.32532775290537}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:45+00","p50":180.015092125,"p95":312.71220842525526,"p99":358.7691954125016}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:46+00","p50":142.636812,"p95":250.49383239089045,"p99":327.24641677913377}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:47+00","p50":96.14335775,"p95":178.7440084525564,"p99":214.38605269567944}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:48+00","p50":184.41511975,"p95":300.4471896945189,"p99":354.805821339009}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:49+00","p50":171.02239237499998,"p95":314.1624889789952,"p99":357.3474065763829}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:50+00","p50":105.16791275,"p95":191.5759042878704,"p99":214.29767983418657}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:51+00","p50":147.996707,"p95":276.0175499015216,"p99":384.23864371620465}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:52+00","p50":115.80518475,"p95":232.8862228398266,"p99":253.5778423838196}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:53+00","p50":87.23289725000001,"p95":149.03163327731204,"p99":170.53616269704818}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:54+00","p50":119.68473575,"p95":213.0709135022924,"p99":242.93984516308592}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:55+00","p50":108.0745905,"p95":194.03301945002482,"p99":211.26841771390582}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:56+00","p50":101.41877025000001,"p95":195.54323051563514,"p99":228.38321312101698}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:57+00","p50":116.79396725000001,"p95":205.7719977117629,"p99":247.67006311397554}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:58+00","p50":80.770152,"p95":150.22897106720364,"p99":216.14457589048362}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:37:59+00","p50":98.29248100000001,"p95":179.1964538175776,"p99":201.22965990165542}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:00+00","p50":157.384051,"p95":320.6814985833535,"p99":373.09811382052516}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:01+00","p50":128.019699375,"p95":229.18827506115895,"p99":255.86604447982026}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:02+00","p50":109.63739375,"p95":242.72557263293137,"p99":276.71883558958075}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:03+00","p50":111.981652375,"p95":201.38065179999847,"p99":244.41134998157787}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:04+00","p50":132.793183375,"p95":216.99146058347557,"p99":266.9090574541638}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:05+00","p50":157.21303875,"p95":269.9945019802091,"p99":342.0042388296108}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:06+00","p50":175.18496274999995,"p95":350.12975082989476,"p99":421.98619505743596}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:07+00","p50":105.312126,"p95":189.14019930692956,"p99":259.3069154403515}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:08+00","p50":127.56426725,"p95":244.00530773908392,"p99":361.7236573760495}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:09+00","p50":142.02786237499998,"p95":288.61047230990437,"p99":371.3689625707383}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:10+00","p50":109.7959075,"p95":188.5847839292388,"p99":236.34858379190445}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:11+00","p50":116.56841424999999,"p95":217.56630235861303,"p99":260.05164910248567}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:12+00","p50":142.30677225,"p95":263.0059346732714,"p99":275.8890333088398}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:13+00","p50":161.75018075,"p95":284.73364983792663,"p99":311.5944037821121}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:14+00","p50":171.84754675,"p95":294.4199457015176,"p99":321.69177510556057}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:15+00","p50":156.19394599999998,"p95":276.80442813266524,"p99":348.67450439680954}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:16+00","p50":144.459568375,"p95":286.5490297591179,"p99":443.21586828348853}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:17+00","p50":214.95977425,"p95":356.23461776314247,"p99":457.9976417575541}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:18+00","p50":145.12581475,"p95":268.8889596198616,"p99":414.3302505994777}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:19+00","p50":119.647234,"p95":240.94366961338937,"p99":294.3237708484001}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:20+00","p50":88.61654962499999,"p95":189.93821073548486,"p99":230.43501553102374}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:21+00","p50":68.93937925,"p95":123.39788667756461,"p99":152.12221119464587}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:22+00","p50":88.76832575,"p95":162.19610377661581,"p99":178.59644478589271}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:23+00","p50":156.868095625,"p95":282.9431760813897,"p99":322.0722005459201}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:24+00","p50":116.38581425,"p95":200.80604728334475,"p99":226.04021254393575}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:25+00","p50":105.797815875,"p95":219.0622379974478,"p99":260.2667714965382}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:26+00","p50":155.00674325,"p95":274.8196609322705,"p99":356.56062448851776}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:27+00","p50":89.6418045,"p95":159.7206555500952,"p99":257.5273081861329}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:28+00","p50":119.5514195,"p95":230.8847993352294,"p99":267.09787791530226}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:29+00","p50":193.92154250000002,"p95":344.52580453241814,"p99":399.3071383861542}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:30+00","p50":164.9921675,"p95":334.9185573452664,"p99":394.6430098304405}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:31+00","p50":81.31760299999999,"p95":157.53159496760452,"p99":177.4120462759304}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:32+00","p50":97.07947775,"p95":166.8663954790479,"p99":195.0732926832838}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:33+00","p50":125.24715175,"p95":232.58842642834784,"p99":325.3389226729326}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:34+00","p50":164.3582925,"p95":331.88579669996506,"p99":403.6713947985246}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:35+00","p50":95.830977875,"p95":177.53012450034197,"p99":205.16646072687794}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:36+00","p50":133.762878,"p95":231.507992026371,"p99":273.4392972690663}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:37+00","p50":140.984448,"p95":257.54953185575295,"p99":298.9030405855026}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:38+00","p50":116.04505725,"p95":209.0724994069035,"p99":285.54292219386053}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:39+00","p50":115.18310825,"p95":211.76646621474367,"p99":280.14397268795085}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:40+00","p50":128.209574,"p95":221.70396525622368,"p99":266.4485832419243}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:41+00","p50":135.96651300000002,"p95":231.12703025049592,"p99":285.30022971929935}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:42+00","p50":129.87082800000002,"p95":242.43311196051405,"p99":310.0377630815096}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:43+00","p50":115.39353925,"p95":214.86192382279825,"p99":283.20476032525636}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:44+00","p50":269.6602055,"p95":490.0686567670713,"p99":583.7279589563927}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:45+00","p50":165.718715,"p95":274.7752261819267,"p99":338.69792182459645}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:46+00","p50":78.995895,"p95":147.53709822068882,"p99":183.66642818162344}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:47+00","p50":100.93827350000001,"p95":209.17176368838943,"p99":279.28723225231886}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:48+00","p50":125.89869062499999,"p95":221.74652522287136,"p99":317.32564305049567}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:49+00","p50":86.61710225,"p95":175.25406527274632,"p99":269.6674786756325}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:50+00","p50":94.071422,"p95":167.61452989153716,"p99":205.6392519209919}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:51+00","p50":157.336746,"p95":254.04876768485855,"p99":306.8280231831565}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:52+00","p50":130.119493,"p95":239.48676846222807,"p99":266.5166707879028}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:53+00","p50":116.552433125,"p95":204.79932182898236,"p99":253.0685399610014}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:54+00","p50":108.01502500000001,"p95":180.03520455768967,"p99":230.13701778236438}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:55+00","p50":167.273475625,"p95":288.1032794164529,"p99":328.0803702562049}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:56+00","p50":123.7301165,"p95":266.9685049782615,"p99":323.5508001217613}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:57+00","p50":127.84831324999999,"p95":215.2154351335504,"p99":236.61160927377225}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:58+00","p50":166.4400925,"p95":325.4469590378056,"p99":411.0278646277256}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:38:59+00","p50":93.9567395,"p95":160.12777325641608,"p99":207.03029004441834}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:00+00","p50":158.816675625,"p95":265.6884139388046,"p99":342.05307690256666}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:01+00","p50":119.45094425,"p95":220.00037047199166,"p99":366.10421831206224}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:02+00","p50":81.019938,"p95":148.32895758781504,"p99":177.5303808744755}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:03+00","p50":115.75609499999999,"p95":210.29741413554407,"p99":238.30444040025472}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:04+00","p50":157.10300475,"p95":257.0113071153836,"p99":300.6690418710308}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:05+00","p50":110.425505,"p95":225.27433839600565,"p99":263.12026974116515}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:06+00","p50":106.71820825,"p95":177.67733633260156,"p99":209.20356568726731}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:07+00","p50":136.435218875,"p95":237.54521272549675,"p99":280.3884134637701}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:08+00","p50":178.97299925,"p95":296.0107112075931,"p99":353.7363075149946}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:09+00","p50":215.38753175,"p95":383.170363000046,"p99":503.776831837729}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:10+00","p50":179.663254,"p95":331.93580173315036,"p99":397.9382579293719}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:11+00","p50":114.34505225,"p95":211.1344582837316,"p99":304.95808749518204}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:12+00","p50":94.593472625,"p95":169.4423209139353,"p99":201.4786516809943}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:13+00","p50":98.204631,"p95":168.52762744386553,"p99":203.00935856868696}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:14+00","p50":105.5908305,"p95":236.8744668058853,"p99":274.75333379815675}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:15+00","p50":169.011895875,"p95":313.8821628307917,"p99":357.2500342359381}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:16+00","p50":162.959905,"p95":266.0841158557624,"p99":339.7923102951741}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:17+00","p50":148.2075105,"p95":261.2696554036134,"p99":319.141071872968}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:18+00","p50":154.6020785,"p95":261.6235915164013,"p99":362.27227886553953}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:19+00","p50":161.016098,"p95":315.321939769021,"p99":381.51819341354656}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:20+00","p50":97.599695125,"p95":174.84293228680474,"p99":231.1634136826098}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:21+00","p50":84.6162285,"p95":166.83585770184047,"p99":206.10468629347204}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:22+00","p50":76.54884287499999,"p95":151.52602226316398,"p99":202.29478558343433}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:23+00","p50":113.653263375,"p95":215.04774452052206,"p99":261.7995499510407}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:24+00","p50":168.7110685,"p95":283.58843705799245,"p99":407.64179766249686}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:25+00","p50":102.669922125,"p95":202.09300246134174,"p99":228.1710120564277}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:26+00","p50":145.949444,"p95":238.8437588694563,"p99":256.5654590339699}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:27+00","p50":116.15533225000001,"p95":284.5696164251221,"p99":330.83835112877154}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:28+00","p50":142.24849475000002,"p95":242.83337129561127,"p99":298.6948471150866}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:29+00","p50":150.76128675,"p95":273.51786629226706,"p99":320.6731338662481}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:30+00","p50":132.604157625,"p95":268.1816924857012,"p99":322.43363177689173}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:31+00","p50":118.4946865,"p95":250.32331985789037,"p99":302.69888880470086}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:32+00","p50":187.8202725,"p95":348.14839207104967,"p99":457.76583795787377}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:33+00","p50":228.88544075,"p95":435.97177667357045,"p99":501.6744154932394}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:34+00","p50":237.896961125,"p95":393.9221971383552,"p99":429.74436165743685}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:35+00","p50":191.82601,"p95":330.3927649985856,"p99":371.1023099483657}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:36+00","p50":191.301383,"p95":346.09559907913115,"p99":429.26723940540313}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:37+00","p50":118.135744125,"p95":239.23381828582836,"p99":277.4311397770903}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:38+00","p50":78.402838,"p95":144.3401245979807,"p99":177.43481047470118}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:39+00","p50":150.270681,"p95":298.6487632392571,"p99":370.1201082821303}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:40+00","p50":152.889489375,"p95":263.37989626350856,"p99":280.8063712001801}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:41+00","p50":158.97943625,"p95":271.3836715359949,"p99":354.32582066359856}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:42+00","p50":84.74546199999999,"p95":155.07489208613754,"p99":179.15980976470854}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:43+00","p50":154.106132625,"p95":280.58177096361027,"p99":319.342891528214}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:44+00","p50":115.3737745,"p95":292.30592330775454,"p99":330.901533852787}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:45+00","p50":89.88674075,"p95":178.1409816361093,"p99":249.78166880996582}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:46+00","p50":106.02971425,"p95":189.5308688347207,"p99":237.3148701986451}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:47+00","p50":131.56561837499999,"p95":229.67714850340718,"p99":286.40063484539655}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:48+00","p50":145.24898975000002,"p95":237.09845579933884,"p99":294.1401854216461}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:49+00","p50":117.06487475,"p95":223.43742898313457,"p99":245.90819812082552}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:50+00","p50":86.69827825,"p95":144.27156606635583,"p99":166.36591920491503}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:51+00","p50":78.35722525,"p95":157.9748019956591,"p99":186.6180366848326}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:52+00","p50":114.09070224999999,"p95":207.85965996786067,"p99":285.85239738043265}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:53+00","p50":138.325375,"p95":263.8737629048869,"p99":334.1221168493645}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:54+00","p50":124.36423350000001,"p95":221.57588228176357,"p99":268.5598757210502}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:55+00","p50":118.9322165,"p95":212.25863084412134,"p99":261.84161603287504}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:56+00","p50":138.65754750000002,"p95":270.94869400470077,"p99":297.88881496250247}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:57+00","p50":129.256927375,"p95":256.862614773632,"p99":392.65722463039685}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:58+00","p50":142.73963924999998,"p95":264.7615829337102,"p99":317.56304596755217}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:39:59+00","p50":113.9991225,"p95":197.81528142398668,"p99":223.93890778758575}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:00+00","p50":153.55616375,"p95":257.3364567136954,"p99":284.0229803385486}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:01+00","p50":142.707433375,"p95":259.66936513709277,"p99":288.2119835221596}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:02+00","p50":151.79964375,"p95":252.99461313460196,"p99":294.9758321716347}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:03+00","p50":188.02394062500002,"p95":330.3276249718827,"p99":414.2409095767899}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:04+00","p50":136.324777875,"p95":278.4569952069595,"p99":309.08886271493867}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:05+00","p50":160.71154775000002,"p95":315.2857112544304,"p99":368.36009770668886}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:06+00","p50":124.53456987499999,"p95":230.2398862773242,"p99":296.48867495933126}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:07+00","p50":125.93848125000001,"p95":246.79585859784746,"p99":296.53599465173147}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:08+00","p50":127.9006285,"p95":241.25823193374566,"p99":259.125909350246}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:09+00","p50":136.89435999999998,"p95":243.93110475185156,"p99":294.84599808761027}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:10+00","p50":167.95576925,"p95":309.07879370406295,"p99":363.6150852314015}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:11+00","p50":115.08279999999999,"p95":210.5753342336676,"p99":293.54615774502133}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:12+00","p50":118.99420412500001,"p95":203.68084789462247,"p99":234.43616921296262}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:13+00","p50":107.749965125,"p95":193.3872992321776,"p99":233.77608004095484}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:14+00","p50":109.36223000000001,"p95":220.82020862037885,"p99":280.07972102473593}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:15+00","p50":217.2089805,"p95":364.8063659352888,"p99":420.002347819149}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:16+00","p50":102.56427550000001,"p95":182.27378840435838,"p99":269.46092169107055}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:17+00","p50":206.67791375000002,"p95":326.0141956956197,"p99":456.02548323781633}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:18+00","p50":287.46370375,"p95":513.077850400024,"p99":625.9606771918207}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:19+00","p50":168.20390112500002,"p95":384.417831829744,"p99":469.16666778052286}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:20+00","p50":129.522899375,"p95":240.12243243988854,"p99":305.1943563650992}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:21+00","p50":143.6340045,"p95":245.50342613468538,"p99":302.3971143024669}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:22+00","p50":127.55543375,"p95":214.7886721927762,"p99":394.8043606546564}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:23+00","p50":100.556007375,"p95":167.45564657041962,"p99":229.61460816024638}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:24+00","p50":121.613229,"p95":229.08943355795134,"p99":308.18553834750605}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:25+00","p50":105.00252187499999,"p95":180.9800296284135,"p99":200.88080119086695}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:26+00","p50":102.765521,"p95":200.30734406080413,"p99":262.18557051481815}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:27+00","p50":111.849952125,"p95":231.6851010928353,"p99":298.3390481798513}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:28+00","p50":101.990479375,"p95":169.27467502903642,"p99":220.0052073220446}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:29+00","p50":100.147446,"p95":238.01573534614147,"p99":275.8243869721141}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:30+00","p50":107.1040505,"p95":184.87893920354654,"p99":219.33649420207976}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:31+00","p50":95.88613525,"p95":174.90402370942383,"p99":190.95595056080606}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:32+00","p50":164.23609525,"p95":286.9506189156022,"p99":360.29427604351287}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:33+00","p50":110.56395524999999,"p95":214.90275192006277,"p99":252.88197259765244}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:34+00","p50":113.92090962500001,"p95":193.99303207190263,"p99":234.74683530870342}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:35+00","p50":112.6548425,"p95":216.59031873853422,"p99":246.86505759159374}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:36+00","p50":91.147389375,"p95":184.82094129256834,"p99":329.8295768367825}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:37+00","p50":105.842871,"p95":191.1376454884592,"p99":219.6415977798033}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:38+00","p50":155.93538825000002,"p95":290.31858307399654,"p99":388.3543847733536}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:39+00","p50":93.78006675,"p95":158.52210570715522,"p99":178.75658863690757}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:40+00","p50":123.722245125,"p95":216.42554067872703,"p99":242.46181425853538}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:41+00","p50":196.088489875,"p95":362.9173902305277,"p99":434.0001885584719}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:42+00","p50":195.6107395,"p95":367.056759721776,"p99":476.2307005814781}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:43+00","p50":174.395918,"p95":287.47821203861025,"p99":328.666862514132}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:44+00","p50":187.85767049999998,"p95":317.2496447828713,"p99":401.54113320951893}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:45+00","p50":86.856569875,"p95":163.2632546460998,"p99":196.7224465923624}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:46+00","p50":148.60648600000002,"p95":282.89838988150944,"p99":358.95263279684923}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:47+00","p50":128.513361125,"p95":252.15333940747215,"p99":305.6610352636974}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:48+00","p50":155.83048575,"p95":308.61699943636614,"p99":399.5821268372192}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:49+00","p50":140.31709949999998,"p95":250.92946075304044,"p99":300.5261323618193}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:50+00","p50":109.398330375,"p95":180.86474469071345,"p99":191.31641397058652}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:51+00","p50":158.34034824999998,"p95":269.4654572414587,"p99":347.68678327043057}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:52+00","p50":142.50908225,"p95":296.5661710421972,"p99":412.39040729521184}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:53+00","p50":130.020934125,"p95":224.39248462472617,"p99":349.1523652424174}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:54+00","p50":155.506500125,"p95":261.44828328318215,"p99":365.9786337066698}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:55+00","p50":179.42872275,"p95":287.7629081670361,"p99":308.92525543609094}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:56+00","p50":103.782803,"p95":275.1291470171785,"p99":345.1833288927403}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:57+00","p50":122.599288875,"p95":242.56221204672582,"p99":285.9382790940728}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:58+00","p50":120.437154125,"p95":229.84404559094477,"p99":293.46414220445394}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:40:59+00","p50":100.05537575,"p95":198.92141858212136,"p99":269.5841532649994}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:00+00","p50":130.92589575,"p95":263.8315689453469,"p99":313.4661788368912}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:01+00","p50":108.32031487500001,"p95":230.08234420584404,"p99":254.57832381085942}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:02+00","p50":100.53958575,"p95":166.8888770874113,"p99":181.6282890338881}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:03+00","p50":128.53746575,"p95":232.4962575568324,"p99":317.1890544362626}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:04+00","p50":90.9879015,"p95":158.7992009445629,"p99":218.94858399650573}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:05+00","p50":104.87180575,"p95":223.73225417292,"p99":301.7320167488422}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:06+00","p50":175.795699,"p95":338.6276557409801,"p99":365.77963417620856}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:07+00","p50":196.157982,"p95":315.9344449894712,"p99":354.93905467628144}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:08+00","p50":117.643074,"p95":229.25230252163786,"p99":323.58050640147997}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:09+00","p50":90.92689625,"p95":150.7711674618516,"p99":196.63711766590595}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:10+00","p50":75.1813405,"p95":140.15124137531328,"p99":173.1269488953705}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:11+00","p50":156.451581,"p95":276.43831520328763,"p99":363.9597981120539}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:12+00","p50":160.92065637500002,"p95":293.0797744702719,"p99":342.5378321988399}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:13+00","p50":217.340713875,"p95":382.96438556682443,"p99":437.78659930052254}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:14+00","p50":132.26165799999998,"p95":235.4813487486001,"p99":294.6244393530002}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:15+00","p50":206.158664875,"p95":320.7088760605945,"p99":380.5579964889343}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:16+00","p50":193.403914875,"p95":332.9161799297578,"p99":369.52557250214795}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:17+00","p50":111.50585812499999,"p95":246.38203906594296,"p99":281.16399441473675}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:18+00","p50":127.636673,"p95":246.87616745718933,"p99":295.2878712614231}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:19+00","p50":164.33276987500003,"p95":319.12710339501217,"p99":384.4240171266842}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:20+00","p50":172.69346787499998,"p95":330.2176174296149,"p99":386.8998323356483}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:21+00","p50":141.94067325,"p95":271.2542536793929,"p99":306.395979046833}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:22+00","p50":113.7751095,"p95":212.10920128783226,"p99":253.15501147991944}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:23+00","p50":111.92516549999999,"p95":185.1720142578249,"p99":209.81439910369514}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:24+00","p50":141.16989475,"p95":229.96755169853688,"p99":330.09901309271623}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:25+00","p50":92.559127375,"p95":156.5400930799685,"p99":173.18178255447316}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:26+00","p50":97.24964262499999,"p95":225.8561114328062,"p99":271.02163234794045}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:27+00","p50":166.594861125,"p95":343.63776181184494,"p99":390.76302464326955}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:28+00","p50":155.619994875,"p95":252.27508973195506,"p99":303.67756354698827}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:29+00","p50":93.8141725,"p95":143.90226724703206,"p99":195.4608362437477}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:30+00","p50":153.40750725,"p95":263.22854130478777,"p99":284.6552075698045}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:31+00","p50":135.97550624999997,"p95":238.28673647949296,"p99":323.78650061753706}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:32+00","p50":88.240857,"p95":162.35316689282953,"p99":271.5627696785057}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:33+00","p50":143.681407875,"p95":244.96253989875598,"p99":279.2041130050376}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:34+00","p50":106.81514387499999,"p95":185.74338418974625,"p99":215.34834177486755}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:35+00","p50":114.85868925,"p95":205.91564683816767,"p99":287.1981648783121}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:36+00","p50":135.407933,"p95":260.9935636860571,"p99":302.86616194302223}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:37+00","p50":231.690099,"p95":401.03046277614374,"p99":440.88506262365104}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:38+00","p50":230.8438985,"p95":422.4053428231901,"p99":487.98417427778764}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:39+00","p50":181.51957225,"p95":392.6831557293416,"p99":486.35841180217597}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:40+00","p50":147.257377,"p95":240.67118070982087,"p99":317.2897436104441}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:41+00","p50":122.85548837500001,"p95":247.9284942389388,"p99":310.3398465759432}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:42+00","p50":144.33150675000002,"p95":279.57424491165494,"p99":315.406764422842}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:43+00","p50":168.7359675,"p95":294.8348129202472,"p99":364.33766824459366}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:44+00","p50":189.537118625,"p95":307.7224384888626,"p99":360.8268662370203}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:45+00","p50":98.1843765,"p95":171.9302319903066,"p99":198.77335217667533}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:46+00","p50":144.3452045,"p95":298.6678791217855,"p99":345.62336735509496}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:47+00","p50":105.4388505,"p95":230.72285036005044,"p99":324.30582002457766}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:48+00","p50":106.31576237499999,"p95":235.91690982844153,"p99":276.0953223092077}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:49+00","p50":178.32235350000002,"p95":318.83427086260315,"p99":375.54812249487685}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:50+00","p50":122.470605,"p95":253.24354918482692,"p99":342.1366228244581}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:51+00","p50":102.15789625,"p95":205.69353675387973,"p99":244.53935265703893}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:52+00","p50":141.39000174999998,"p95":250.430393941365,"p99":299.85286956872653}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:53+00","p50":151.78941325,"p95":257.3216500399931,"p99":321.59512627494}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:54+00","p50":164.89939025,"p95":329.99344127040126,"p99":425.9760670131607}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:55+00","p50":143.30167725,"p95":245.44347580948906,"p99":262.70404818537094}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:56+00","p50":110.01073862499999,"p95":280.96064987996544,"p99":345.8144251350279}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:57+00","p50":141.53272725,"p95":241.59128897637535,"p99":294.45066305665233}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:58+00","p50":156.3382155,"p95":272.64634370367116,"p99":320.23479364907934}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:41:59+00","p50":148.07724762499998,"p95":264.4167006878803,"p99":278.3896774760377}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:00+00","p50":140.787592,"p95":322.7627679976897,"p99":372.25440489812473}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:01+00","p50":95.71082849999999,"p95":191.23092309812952,"p99":217.1293403185246}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:02+00","p50":110.80178925,"p95":184.94247150355596,"p99":222.06633525641084}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:03+00","p50":127.8753435,"p95":233.02334958903646,"p99":263.917859669796}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:04+00","p50":96.21209400000001,"p95":180.4118421812197,"p99":230.470033870584}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:05+00","p50":127.616092125,"p95":234.94939925549977,"p99":290.5120697148728}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:06+00","p50":122.448014,"p95":235.56948054654885,"p99":276.8066741812019}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:07+00","p50":101.99734325,"p95":246.3306087554867,"p99":306.4901524818268}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:08+00","p50":179.48170975,"p95":297.36307043819625,"p99":330.4799485638623}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:09+00","p50":154.48938099999998,"p95":240.95570073801565,"p99":278.2068274672165}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:10+00","p50":151.903418875,"p95":264.1781482331685,"p99":327.77701225334715}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:11+00","p50":105.19737549999999,"p95":178.96667670211576,"p99":222.43602816604087}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:12+00","p50":117.66714075,"p95":202.8416588660174,"p99":230.84688507774615}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:13+00","p50":210.326336,"p95":334.7969624487963,"p99":409.9795503330116}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:14+00","p50":145.336702875,"p95":285.3160289602604,"p99":364.9390115884519}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:15+00","p50":98.6081035,"p95":168.63798565464614,"p99":188.50792227329418}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:16+00","p50":68.82574349999999,"p95":122.52827270473277,"p99":153.60788358651732}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:17+00","p50":70.534524625,"p95":146.41988561759035,"p99":177.67083397401882}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:18+00","p50":158.299693875,"p95":261.4633548453215,"p99":373.6140856297176}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:19+00","p50":151.89453100000003,"p95":241.7410138901775,"p99":306.3774793957248}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:20+00","p50":141.546152,"p95":283.51168840125877,"p99":354.63138895826125}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:21+00","p50":181.24983462499998,"p95":311.5045031770151,"p99":372.62580057138064}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:22+00","p50":135.115877,"p95":247.25302402984985,"p99":295.9554203998105}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:23+00","p50":168.009402,"p95":346.5794267111067,"p99":400.0685648796325}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:24+00","p50":144.2002445,"p95":266.26512601330523,"p99":354.4671667764406}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:25+00","p50":114.10687999999999,"p95":197.95624934377338,"p99":236.48580868994713}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:26+00","p50":116.99417025,"p95":209.1492365504467,"p99":236.13013447836755}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:27+00","p50":127.724283875,"p95":191.30484734164565,"p99":244.04126736116456}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:28+00","p50":141.421897,"p95":236.0743603511213,"p99":286.0111772160733}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:29+00","p50":99.291600875,"p95":179.56632304613643,"p99":222.30805611932516}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:30+00","p50":80.09297649999999,"p95":144.36113495628905,"p99":185.46303933191277}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:31+00","p50":114.54049462500001,"p95":204.64329540967387,"p99":240.96238848944617}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:32+00","p50":67.421089,"p95":116.23495397450019,"p99":159.20688061552045}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:33+00","p50":151.75763137500002,"p95":275.93285470892397,"p99":334.22780055234125}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:34+00","p50":136.593133,"p95":275.2873060089636,"p99":327.5000914015083}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:35+00","p50":98.803046,"p95":178.91978888925135,"p99":234.68618458147574}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:36+00","p50":101.160566875,"p95":178.9719456482085,"p99":247.72400011359525}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:37+00","p50":133.64249775000002,"p95":212.2500193170936,"p99":254.21171353640844}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:38+00","p50":101.09957750000001,"p95":192.11722132946772,"p99":237.10030998756935}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:39+00","p50":97.2257985,"p95":176.2678358463016,"p99":218.38403702192832}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:40+00","p50":114.16709600000002,"p95":197.74192172777188,"p99":241.18630036014483}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:41+00","p50":110.19398749999999,"p95":215.94116323853433,"p99":262.61262896541456}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:42+00","p50":122.3616385,"p95":211.7527395085394,"p99":232.6224993485012}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:43+00","p50":108.54346,"p95":230.50310562212968,"p99":302.19922188510947}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:44+00","p50":149.11396487500002,"p95":259.89275098648386,"p99":295.85048340439795}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:45+00","p50":95.83802975,"p95":166.38250375804287,"p99":215.1018267629578}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:46+00","p50":90.05515700000001,"p95":164.27234395928383,"p99":179.53590618324088}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:47+00","p50":104.71407274999999,"p95":199.27098786404218,"p99":241.15705894620655}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:48+00","p50":144.948047,"p95":259.0869776833782,"p99":307.9825096678009}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:49+00","p50":130.45518175,"p95":250.78980639173938,"p99":290.7367378213613}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:50+00","p50":171.570303125,"p95":322.8860095414251,"p99":385.0372922422576}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:51+00","p50":101.71850549999999,"p95":190.32114798940205,"p99":282.30636498170855}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:52+00","p50":132.071278,"p95":259.4468439195464,"p99":309.8199570349955}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:53+00","p50":196.9161565,"p95":370.62781332494677,"p99":413.0886349140458}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:54+00","p50":166.14251775,"p95":274.9088496090621,"p99":320.36315466350675}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:55+00","p50":162.0083055,"p95":271.4109266959865,"p99":333.42131654803654}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:56+00","p50":168.677597875,"p95":322.6641363102864,"p99":428.8239863729813}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:57+00","p50":144.20317075000003,"p95":318.2990687422636,"p99":382.5951156477592}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:58+00","p50":154.177346,"p95":285.1249423620785,"p99":311.3991094238355}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:42:59+00","p50":121.25967399999999,"p95":242.51815689349937,"p99":281.93261933107755}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:00+00","p50":98.74610475,"p95":175.28721181880738,"p99":216.54953192986082}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:01+00","p50":106.9196525,"p95":185.28778224592375,"p99":212.69116978652954}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:02+00","p50":88.50364450000001,"p95":151.71149420102213,"p99":175.14325391744993}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:03+00","p50":90.08600437499999,"p95":226.8032655219506,"p99":267.3935036701837}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:04+00","p50":100.91928300000001,"p95":208.38050100937872,"p99":246.30503027765656}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:05+00","p50":144.16530175000003,"p95":283.29757053431655,"p99":338.51624933920476}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:06+00","p50":127.4885535,"p95":247.3411936690247,"p99":288.80225577584554}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:07+00","p50":84.36671462500001,"p95":157.6829206928162,"p99":194.05123172809698}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:08+00","p50":145.974749,"p95":260.3140321246738,"p99":350.51645582276296}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:09+00","p50":208.425216875,"p95":479.8464607752978,"p99":532.8743521381471}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:10+00","p50":216.8992325,"p95":389.9132560865905,"p99":445.14080665451905}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:11+00","p50":99.91466325,"p95":163.71042689219237,"p99":202.56403205495263}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:12+00","p50":126.8637235,"p95":250.46274618629383,"p99":305.9493906240511}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:13+00","p50":117.671779,"p95":221.832016447711,"p99":266.0157059769955}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:14+00","p50":101.695671,"p95":174.90136747725154,"p99":203.62020941742944}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:15+00","p50":90.8117115,"p95":196.58095317338538,"p99":240.67018908339787}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:16+00","p50":112.19624875,"p95":195.58741137052218,"p99":265.8970917029908}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:17+00","p50":117.66450075,"p95":211.91172119355738,"p99":242.03704786077284}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:18+00","p50":140.393042,"p95":215.46614418907998,"p99":258.3501643394842}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:19+00","p50":129.422758,"p95":253.56879691624783,"p99":274.30273072361564}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:20+00","p50":115.2284765,"p95":205.82220969689178,"p99":258.109011710639}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:21+00","p50":138.687999875,"p95":271.31773043971657,"p99":319.4237044712505}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:22+00","p50":124.114554,"p95":282.24134288862655,"p99":339.1857482997246}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:23+00","p50":111.800628625,"p95":211.25076196603783,"p99":256.59728326316906}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:24+00","p50":105.451098875,"p95":182.86894343742063,"p99":220.01734106959105}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:25+00","p50":120.41594125,"p95":262.4139085955248,"p99":309.3874269051442}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:26+00","p50":155.936527,"p95":259.7810648224366,"p99":296.67006924191924}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:27+00","p50":119.58099775,"p95":204.86731294597786,"p99":267.0491820517447}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:28+00","p50":133.89506849999998,"p95":210.75879367794562,"p99":301.0972166909142}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:29+00","p50":126.0275965,"p95":224.39143787002288,"p99":253.25708338815332}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:30+00","p50":99.454323,"p95":187.88091212300634,"p99":212.13534531074527}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:31+00","p50":94.30339137499999,"p95":179.2561887223585,"p99":212.16614303650425}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:32+00","p50":100.54634349999999,"p95":173.0199117983377,"p99":204.05976335522175}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:33+00","p50":121.166137625,"p95":225.657349642066,"p99":263.9755463127992}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:34+00","p50":99.764311875,"p95":171.02580764781177,"p99":192.2766165690465}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:35+00","p50":81.10258737500001,"p95":136.08788198367108,"p99":160.07931854304505}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:36+00","p50":144.052473,"p95":251.45445227487278,"p99":285.3806999911418}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:37+00","p50":103.25430525,"p95":186.42800463953307,"p99":213.44282404223347}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:38+00","p50":213.34675299999998,"p95":337.5314787986807,"p99":432.60918193550253}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:39+00","p50":231.029154,"p95":368.7317897096274,"p99":412.0217938202462}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:40+00","p50":123.39937900000001,"p95":300.8939314152922,"p99":368.5655765871384}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:41+00","p50":155.26636575,"p95":255.04183774945236,"p99":284.1115950855961}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:42+00","p50":103.300544125,"p95":169.09924493256264,"p99":213.65235053006984}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:43+00","p50":107.11964449999999,"p95":189.21814017299045,"p99":235.05201402496792}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:44+00","p50":111.62721250000001,"p95":204.17993328764487,"p99":229.12659485162735}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:45+00","p50":92.55482262500001,"p95":178.96202083255164,"p99":210.1036834380946}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:46+00","p50":102.18536025,"p95":179.21681411196613,"p99":214.1507333300781}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:47+00","p50":86.3876245,"p95":159.09844481367588,"p99":308.10585962104034}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:48+00","p50":138.48176725,"p95":222.3731437528806,"p99":267.9870061145076}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:49+00","p50":82.40877087499999,"p95":147.30258498278593,"p99":175.3692997468736}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:50+00","p50":102.31804325,"p95":159.3052272611103,"p99":190.46212463482667}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:51+00","p50":89.2117345,"p95":169.7192795382054,"p99":236.50584604976297}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:52+00","p50":150.55866250000003,"p95":265.9022331368868,"p99":365.93544949280067}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:53+00","p50":129.6061965,"p95":237.57086301064777,"p99":309.6121095512161}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:54+00","p50":135.92783137499998,"p95":246.1842087373398,"p99":279.74366551418734}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:55+00","p50":110.12261050000001,"p95":239.19525432550287,"p99":286.5456068910713}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:56+00","p50":99.236586,"p95":171.18650904216216,"p99":254.64027920075512}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:57+00","p50":154.73401325,"p95":255.52744732979227,"p99":283.95553653929426}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:58+00","p50":127.16723674999999,"p95":226.93460485981822,"p99":309.4578824070799}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:43:59+00","p50":114.37227337499999,"p95":212.99435112545908,"p99":307.8495307094693}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:00+00","p50":110.57957999999999,"p95":203.42232285920716,"p99":245.46689932424925}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:01+00","p50":91.2021435,"p95":156.8916889517758,"p99":179.37070115738774}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:02+00","p50":134.55258350000003,"p95":236.02471965083777,"p99":307.5775922805572}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:03+00","p50":151.04973712499998,"p95":262.2238551400672,"p99":294.0592836158151}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:04+00","p50":138.92280399999999,"p95":231.29003487895977,"p99":253.92683750426102}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:05+00","p50":125.102159,"p95":210.57695181701814,"p99":285.98050815102295}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:06+00","p50":94.14099725,"p95":174.32285289662283,"p99":233.8733605067291}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:07+00","p50":89.84799924999999,"p95":149.9514668417697,"p99":201.00474586262487}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:08+00","p50":159.505935375,"p95":298.01378981376206,"p99":336.7817176407025}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:09+00","p50":128.0984095,"p95":221.629567262249,"p99":291.15077149405573}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:10+00","p50":118.56984825,"p95":223.92029054681092,"p99":253.3344918672924}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:11+00","p50":147.0766885,"p95":278.97957026017855,"p99":354.65444686960694}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:12+00","p50":139.322213,"p95":232.46191676707602,"p99":274.0915827755108}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:13+00","p50":85.8706705,"p95":159.7854236907656,"p99":189.04863671471216}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:14+00","p50":77.640807125,"p95":136.51460178830885,"p99":150.0771253117924}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:15+00","p50":89.62026550000002,"p95":151.26294441092176,"p99":161.437068589005}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:16+00","p50":108.254706,"p95":203.3909229578966,"p99":264.41472613225244}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:17+00","p50":161.2212345,"p95":271.77463009681594,"p99":335.6336693782621}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:18+00","p50":176.58207149999998,"p95":307.5038041796062,"p99":409.13890873446985}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:19+00","p50":110.11551750000001,"p95":207.06174159773562,"p99":264.11776913065626}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:20+00","p50":91.19986300000001,"p95":160.70063218692852,"p99":177.88204351167582}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:21+00","p50":144.53578925,"p95":249.61031745475773,"p99":297.95023765572637}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:22+00","p50":141.723673125,"p95":263.3165445205465,"p99":336.07346288469313}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:23+00","p50":216.869627,"p95":368.43139441035197,"p99":412.9261176435046}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:24+00","p50":171.032509,"p95":370.8439584333666,"p99":479.2870731905567}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:25+00","p50":145.07655999999997,"p95":266.08702110801653,"p99":322.601401004633}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:26+00","p50":101.46408,"p95":187.64609417895173,"p99":249.33707759716796}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:27+00","p50":94.9844925,"p95":162.79663016263973,"p99":223.50950732928658}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:28+00","p50":91.478148,"p95":174.3863455980599,"p99":191.4736336963272}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:29+00","p50":96.08230275,"p95":170.83020717254138,"p99":219.35906099091196}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:30+00","p50":91.79307575,"p95":178.96728356297112,"p99":224.68024010671996}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:31+00","p50":131.817676,"p95":216.84978735293893,"p99":292.94693858100703}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:32+00","p50":223.6321795,"p95":410.73295489634035,"p99":515.0850010337754}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:33+00","p50":247.7949325,"p95":405.35100829550026,"p99":452.4405294550386}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:34+00","p50":109.969272125,"p95":231.82488188842737,"p99":307.15040329442337}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:35+00","p50":87.91177025,"p95":151.8676565254165,"p99":163.8369356883483}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:36+00","p50":115.69512925000001,"p95":186.06248317580676,"p99":227.20833044445516}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:37+00","p50":130.10904916666667,"p95":281.3871177530346,"p99":329.2653884988985}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:38+00","p50":130.9516855,"p95":241.4651282848912,"p99":308.18640571358657}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:39+00","p50":140.40034175,"p95":237.95262093512702,"p99":277.3618458281465}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:40+00","p50":114.47163975000001,"p95":225.0798534787502,"p99":269.1456775704689}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:41+00","p50":128.62674487499999,"p95":234.10959833487075,"p99":291.8265991073849}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:42+00","p50":133.74399175,"p95":227.22619892207706,"p99":330.5771852720468}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:43+00","p50":153.404875,"p95":253.31029230709981,"p99":289.22487938775686}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:44+00","p50":103.40916212500002,"p95":207.56793106595998,"p99":280.5059781116545}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:45+00","p50":77.044896,"p95":151.5907638802374,"p99":179.39354077498052}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:46+00","p50":72.569646,"p95":116.53805492147232,"p99":164.8607832717762}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:47+00","p50":82.03471562499999,"p95":186.37280567995506,"p99":246.52898615761328}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:48+00","p50":147.371935,"p95":288.8765640523837,"p99":349.0539718282461}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:49+00","p50":97.334225,"p95":174.92159978294205,"p99":214.32764247509957}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:50+00","p50":158.52224437499999,"p95":295.7939410250684,"p99":372.0809850978093}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:51+00","p50":142.38109137499998,"p95":329.0406198056769,"p99":392.6612817624831}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:52+00","p50":129.788458,"p95":248.62017183192282,"p99":336.97430313498216}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:53+00","p50":154.51512275000002,"p95":353.3550922488358,"p99":431.3406532626953}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:54+00","p50":164.1928395,"p95":352.719691712713,"p99":404.29913137688635}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:55+00","p50":111.89381175,"p95":207.6050741588974,"p99":242.8327057235031}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:56+00","p50":128.532712125,"p95":236.97746554740374,"p99":254.7880504129603}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:57+00","p50":273.34712625,"p95":502.0316772212541,"p99":588.0479301910161}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:58+00","p50":148.34029974999999,"p95":366.6159704474291,"p99":496.1870063027363}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:44:59+00","p50":141.85357299999998,"p95":239.14735477427638,"p99":325.68533352864074}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:00+00","p50":151.809842,"p95":270.53780604701615,"p99":323.4212838583221}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:01+00","p50":154.82157075,"p95":311.63859046577,"p99":401.3785004987259}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:02+00","p50":227.89166799999998,"p95":365.8333434975443,"p99":443.0647073951688}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:03+00","p50":128.864561875,"p95":268.71279197812385,"p99":351.5712776953185}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:04+00","p50":127.3448535,"p95":224.0622871636324,"p99":277.84009613737777}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:05+00","p50":98.02853049999999,"p95":179.23597599013902,"p99":215.67216550413514}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:06+00","p50":130.327724,"p95":226.66989375025295,"p99":276.6405706711841}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:07+00","p50":96.20616875,"p95":166.29557374678885,"p99":191.52573982815744}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:08+00","p50":93.097402125,"p95":207.361048133781,"p99":257.83394557287}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:09+00","p50":123.6092435,"p95":204.8431126177528,"p99":289.0762628391347}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:10+00","p50":102.13948575,"p95":171.34678935942148,"p99":223.42944805186795}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:11+00","p50":98.25140999999999,"p95":168.76778432369804,"p99":225.2021098369598}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:12+00","p50":114.81741425,"p95":238.32296920090246,"p99":356.84568921344567}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:13+00","p50":146.96959750000002,"p95":274.38840025495864,"p99":314.334443486598}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:14+00","p50":200.329405,"p95":295.70785536070247,"p99":335.0934206383057}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:15+00","p50":193.43503,"p95":303.40490909326456,"p99":382.14356417680835}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:16+00","p50":175.499666,"p95":296.93390429593717,"p99":327.4486573731547}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:17+00","p50":193.46678125,"p95":303.0033824256601,"p99":343.03695791005083}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:18+00","p50":162.3544865,"p95":288.8182284863353,"p99":365.32326485476824}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:19+00","p50":148.70781375,"p95":253.0540110355438,"p99":286.89337927136233}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:20+00","p50":111.9068135,"p95":211.6011632271887,"p99":266.1909768937964}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:21+00","p50":197.79366687500004,"p95":343.74937107047987,"p99":376.8854101760642}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:22+00","p50":154.48239725,"p95":281.7464615606017,"p99":344.1655974600611}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:23+00","p50":90.8063975,"p95":159.34523038991642,"p99":190.80784507170557}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:24+00","p50":92.81930249999999,"p95":198.04157685904025,"p99":226.58493774172592}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:25+00","p50":99.6580845,"p95":203.58530611493373,"p99":260.13748142213484}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:26+00","p50":127.838764375,"p95":228.45671809815713,"p99":285.51493860568837}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:27+00","p50":124.09808462499998,"p95":278.8036772153276,"p99":359.8171137292061}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:28+00","p50":156.98051625,"p95":299.9866025988537,"p99":344.5842577486973}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:29+00","p50":151.46376075,"p95":263.40569837267833,"p99":338.53329009361556}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:30+00","p50":144.84380449999998,"p95":285.52761328190206,"p99":354.54489235211275}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:31+00","p50":101.4277345,"p95":172.05395192488396,"p99":193.5401244422388}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:32+00","p50":161.95931275,"p95":276.1538104332579,"p99":383.2020282273085}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:33+00","p50":139.009254875,"p95":264.42480838616945,"p99":402.99496327359435}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:34+00","p50":83.27464275,"p95":167.3402137002728,"p99":214.63034933302544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:35+00","p50":136.45699375,"p95":223.93174762700613,"p99":294.7746878282847}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:36+00","p50":79.981556375,"p95":134.36538536490607,"p99":179.0789823672087}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:37+00","p50":93.851133,"p95":153.08870559753544,"p99":175.0438742830405}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:38+00","p50":101.89264825000001,"p95":182.82700685251294,"p99":257.53797734140255}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:39+00","p50":148.88002450000002,"p95":281.6686623068929,"p99":316.8203752621622}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:40+00","p50":127.98199175000002,"p95":253.24299568858248,"p99":366.8163550254671}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:41+00","p50":139.69501449999998,"p95":237.8194111724909,"p99":317.2520162330665}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:42+00","p50":112.25914112500001,"p95":202.15612112838352,"p99":258.70530278486586}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:43+00","p50":193.06880825000002,"p95":335.0083985412396,"p99":389.2017109910836}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:44+00","p50":156.673806,"p95":297.06224389818607,"p99":349.9320082163853}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:45+00","p50":111.08392624999999,"p95":178.31011044504177,"p99":216.1123520808644}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:46+00","p50":113.29639800000001,"p95":218.7351682635733,"p99":284.82257813775686}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:47+00","p50":82.643023,"p95":150.83175167232346,"p99":195.16557317203092}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:48+00","p50":131.47161025,"p95":236.53559630877245,"p99":300.76197675224256}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:49+00","p50":193.201112,"p95":345.04748059004356,"p99":390.0549618068342}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:50+00","p50":174.250716,"p95":293.65703754790684,"p99":333.23204831179805}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:51+00","p50":148.96329925,"p95":285.099314589292,"p99":324.7075702625136}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:52+00","p50":72.89207125,"p95":231.48957953208637,"p99":313.6319255236435}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:53+00","p50":112.2935885,"p95":233.99653202728584,"p99":281.8582639525952}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:54+00","p50":216.46223387499998,"p95":355.2705244887665,"p99":449.2564780158556}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:55+00","p50":206.266161375,"p95":350.74124088832417,"p99":419.69320975022674}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:56+00","p50":200.45121775,"p95":390.42522717938704,"p99":475.3680017949486}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:57+00","p50":127.29120325,"p95":231.11788617517828,"p99":275.7034049391241}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:58+00","p50":124.33156699999999,"p95":220.39672803620368,"p99":245.58714174468113}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:45:59+00","p50":94.75080975,"p95":149.98064052131426,"p99":168.55727104488327}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:00+00","p50":81.96945775,"p95":148.16077856303426,"p99":168.0265698072965}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:01+00","p50":146.895323625,"p95":249.87923657840878,"p99":288.4859002938928}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:02+00","p50":114.017781,"p95":244.73301418479454,"p99":385.84544200403616}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:03+00","p50":99.19644675,"p95":182.48579130125998,"p99":214.40423439637945}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:04+00","p50":114.39084737499999,"p95":220.04751904256213,"p99":277.2714807418656}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:05+00","p50":101.5173695,"p95":188.00573005898858,"p99":240.05136404623414}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:06+00","p50":103.63723774999998,"p95":183.63698785679406,"p99":200.2796717285266}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:07+00","p50":138.52390524999998,"p95":244.04160464400744,"p99":289.16768993190243}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:08+00","p50":174.74991775,"p95":295.9402841974542,"p99":337.24571776889036}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:09+00","p50":100.52245537499999,"p95":181.42297499340944,"p99":187.69693039923334}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:10+00","p50":104.35289549999999,"p95":193.61413014909115,"p99":226.41523657711483}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:11+00","p50":159.86354525000002,"p95":309.99511890237096,"p99":374.6054399558416}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:12+00","p50":186.78027475,"p95":350.56610769185426,"p99":404.7614324710688}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:13+00","p50":127.5967225,"p95":243.89512809551812,"p99":345.65351358241276}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:14+00","p50":103.26010199999999,"p95":196.75917411392655,"p99":245.08809780254032}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:15+00","p50":84.95083025,"p95":143.43637181149413,"p99":176.50628817827513}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:16+00","p50":107.07618912499998,"p95":190.10611177007343,"p99":208.4306353850584}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:17+00","p50":125.90609524999999,"p95":213.51545625814092,"p99":257.5813049891863}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:18+00","p50":108.8234265,"p95":199.45036091024423,"p99":238.74948298450568}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:19+00","p50":116.30308575,"p95":243.4172888106686,"p99":295.6704910221381}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:20+00","p50":160.06577099999998,"p95":271.47067227475645,"p99":353.0827091058068}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:21+00","p50":130.95216025000002,"p95":215.33202362921168,"p99":298.4797049025736}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:22+00","p50":170.0615895,"p95":316.3868234962864,"p99":350.87721879409247}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:23+00","p50":160.0407085,"p95":259.4868257632861,"p99":344.54012697182515}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:24+00","p50":126.83937025,"p95":222.73997437062968,"p99":280.7996306646147}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:25+00","p50":210.03403075,"p95":354.0650150748729,"p99":378.9694853874054}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:26+00","p50":181.82181,"p95":360.6052567200076,"p99":548.8554881368403}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:27+00","p50":84.731615,"p95":145.25815029418038,"p99":154.78591671372985}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:28+00","p50":205.065407125,"p95":366.1823219022646,"p99":414.71976048528074}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:29+00","p50":119.3064105,"p95":229.92042764411522,"p99":288.32742882036615}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:30+00","p50":119.27640787499999,"p95":229.5146669968439,"p99":267.60432755031036}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:31+00","p50":155.09592099999998,"p95":271.6352583556115,"p99":313.7625307780881}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:32+00","p50":112.368894,"p95":257.87440330270863,"p99":312.20888283084105}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:33+00","p50":115.641684375,"p95":194.72834639408458,"p99":239.56081262937283}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:34+00","p50":126.57340025,"p95":225.54810577835167,"p99":279.574628439775}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:35+00","p50":163.42332712500001,"p95":280.4578620995911,"p99":394.80225048857113}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:36+00","p50":149.9994665,"p95":269.61896495110034,"p99":339.9599184973355}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:37+00","p50":119.04566712500001,"p95":203.02155494689273,"p99":219.94334861241697}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:38+00","p50":144.78784375,"p95":265.24966716060857,"p99":297.5737469482851}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:39+00","p50":86.826,"p95":136.67239254454327,"p99":189.0674115946102}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:40+00","p50":102.198232875,"p95":211.97671741222786,"p99":289.825193544127}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:41+00","p50":145.387030625,"p95":244.58986065586228,"p99":356.83691385336397}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:42+00","p50":219.695472,"p95":371.7757312220302,"p99":436.41890832040286}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:43+00","p50":232.72891925,"p95":398.0944623070617,"p99":529.2788928770761}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:44+00","p50":201.84708849999998,"p95":319.92407981682896,"p99":355.5796339894543}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:45+00","p50":153.890911,"p95":295.2765054924749,"p99":324.4775373170724}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:46+00","p50":120.8339605,"p95":219.04834395043028,"p99":289.0672029050767}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:47+00","p50":193.2531645,"p95":309.12280415643403,"p99":355.11686430462265}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:48+00","p50":131.584592,"p95":253.83492135650636,"p99":295.7105715129089}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:49+00","p50":123.89436025,"p95":233.16953991569673,"p99":279.6293415869789}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:50+00","p50":164.5324255,"p95":288.73135754275967,"p99":370.0955298803764}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:51+00","p50":162.48080575,"p95":316.02359783918,"p99":437.22611782495693}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:52+00","p50":142.87211150000002,"p95":239.3420650567231,"p99":325.86580151606273}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:53+00","p50":101.63947525,"p95":214.6544844113134,"p99":260.0387163752568}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:54+00","p50":108.73811387500001,"p95":180.8693438429191,"p99":209.15902539713193}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:55+00","p50":127.52550987500001,"p95":229.16561517444921,"p99":279.2028813717103}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:56+00","p50":104.18456850000001,"p95":181.83665102393485,"p99":265.942648975626}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:57+00","p50":125.570763,"p95":208.47589223571427,"p99":302.673523665636}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:58+00","p50":173.776108625,"p95":309.2445375267485,"p99":351.09571718380596}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:46:59+00","p50":148.349984875,"p95":268.2755163264241,"p99":314.7196049008124}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:00+00","p50":103.67545512500001,"p95":259.7884184819403,"p99":289.9082359485779}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:01+00","p50":106.30636974999999,"p95":195.6386908775877,"p99":224.4699936914058}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:02+00","p50":100.13808399999999,"p95":180.29646189564616,"p99":204.3091240020137}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:03+00","p50":119.81637775,"p95":194.5077057162744,"p99":239.1596881597023}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:04+00","p50":126.12755075000001,"p95":230.2102255648434,"p99":253.6696904222727}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:05+00","p50":112.44873974999999,"p95":205.0704338207363,"p99":229.15154012490225}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:06+00","p50":151.47872237500002,"p95":257.1450015248069,"p99":348.30733049414374}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:07+00","p50":140.54049650000002,"p95":251.71831160766232,"p99":301.01761637816145}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:08+00","p50":115.954155125,"p95":207.69182146457166,"p99":258.2960148813415}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:09+00","p50":126.65043,"p95":242.9393694898188,"p99":295.6696568109655}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:10+00","p50":186.10506900000001,"p95":337.97028937208125,"p99":396.7932612202835}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:11+00","p50":109.61577375,"p95":169.94996819668947,"p99":241.51055579355526}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:12+00","p50":99.009894,"p95":176.35112621205568,"p99":210.1656707166605}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:13+00","p50":114.915984625,"p95":213.15873340535057,"p99":273.6388327104201}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:14+00","p50":117.07201524999999,"p95":243.5826520811076,"p99":266.230870290369}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:15+00","p50":143.45611200000002,"p95":236.2174740643177,"p99":266.61686243955995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:16+00","p50":146.922187625,"p95":295.67317037846345,"p99":335.34103148353984}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:17+00","p50":115.535618,"p95":219.92863704837453,"p99":273.92018721714544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:18+00","p50":144.931917625,"p95":259.6348978439655,"p99":323.3420325335138}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:19+00","p50":165.023665,"p95":310.1715076363535,"p99":408.1780848467026}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:20+00","p50":100.447191625,"p95":216.2699708411653,"p99":241.50724525981903}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:21+00","p50":140.76204775,"p95":231.9101198104691,"p99":323.0569237442064}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:22+00","p50":129.617382,"p95":212.6963691984412,"p99":257.592035563437}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:23+00","p50":125.00705525000001,"p95":204.2636032351594,"p99":224.24053454949475}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:24+00","p50":108.884537,"p95":219.82776114234716,"p99":249.7907731136608}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:25+00","p50":173.446315,"p95":312.6675116532514,"p99":356.0896426826563}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:26+00","p50":97.4606095,"p95":174.3656844659461,"p99":231.18043530585385}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:27+00","p50":189.049236,"p95":343.8988204843871,"p99":382.3146383865435}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:28+00","p50":186.68699025,"p95":352.3641351917647,"p99":418.4105367978842}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:29+00","p50":98.68324900000002,"p95":162.16381607322097,"p99":193.75705866608428}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:30+00","p50":70.091701,"p95":166.10182599590777,"p99":201.91405564452936}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:31+00","p50":90.433844375,"p95":180.5099133841576,"p99":220.64199925866532}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:32+00","p50":119.92668937500001,"p95":243.5139929201196,"p99":318.44026849600885}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:33+00","p50":81.35050025,"p95":192.25256756661676,"p99":280.8548857368589}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:34+00","p50":165.5897295,"p95":307.0957378450988,"p99":366.52222070617483}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:35+00","p50":135.345382,"p95":288.3535589198947,"p99":357.0509297669507}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:36+00","p50":72.257410625,"p95":137.24092756063436,"p99":167.671653052516}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:37+00","p50":125.45103025,"p95":223.94546459160935,"p99":296.8866043176198}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:38+00","p50":151.4957185,"p95":259.38772247716287,"p99":357.3164632471161}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:39+00","p50":80.850338,"p95":152.23382641482462,"p99":183.2627876864257}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:40+00","p50":89.732975625,"p95":155.66651697890006,"p99":171.5909818804531}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:41+00","p50":111.365389375,"p95":228.4150861670974,"p99":268.67164048228665}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:42+00","p50":101.297550125,"p95":207.7675988990016,"p99":261.71886355362324}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:43+00","p50":158.973789625,"p95":246.3167629179031,"p99":327.9781784235101}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:44+00","p50":153.87151174999997,"p95":253.85119265312576,"p99":293.3733993349569}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:45+00","p50":119.4168475,"p95":216.34754491494547,"p99":254.49294624739122}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:46+00","p50":79.62130537499999,"p95":151.21946329128957,"p99":179.06034340946985}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:47+00","p50":110.28215925,"p95":178.4521833291817,"p99":207.07978707728887}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:48+00","p50":125.99117462500001,"p95":283.5430769330518,"p99":399.08318335847736}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:49+00","p50":115.34410787499999,"p95":224.1116938705213,"p99":281.6817227007041}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:50+00","p50":222.48016125,"p95":386.0301856834216,"p99":526.5229716894693}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:51+00","p50":220.2514165,"p95":406.35824003892543,"p99":496.2448071918445}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:52+00","p50":120.207693,"p95":251.99158698441505,"p99":319.2247447882614}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:53+00","p50":128.512327125,"p95":237.02073922059913,"p99":251.13989422816658}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:54+00","p50":109.696513125,"p95":194.239256824687,"p99":206.21201082844186}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:55+00","p50":130.665392,"p95":232.28797864113463,"p99":324.80650905060435}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:56+00","p50":204.48163,"p95":397.55466337186886,"p99":492.39878527940084}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:57+00","p50":235.07199025,"p95":427.12363942077224,"p99":467.3937595137558}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:58+00","p50":203.96837699999998,"p95":358.6587107278748,"p99":488.56959830869295}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:47:59+00","p50":192.25415425,"p95":364.17250509893887,"p99":411.8134100592759}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:00+00","p50":119.23407,"p95":209.48488538216304,"p99":234.0052297508559}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:01+00","p50":171.140021875,"p95":321.5045328702277,"p99":372.7999712751386}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:02+00","p50":119.28937375000001,"p95":256.9641438794949,"p99":335.7554776223082}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:03+00","p50":112.54241024999999,"p95":227.04854971467233,"p99":262.1845976902771}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:04+00","p50":189.655393,"p95":305.3411644798813,"p99":368.4413139127426}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:05+00","p50":162.28333987500002,"p95":304.77826085755623,"p99":347.97324985936206}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:06+00","p50":157.57393712500001,"p95":299.0422332019929,"p99":340.945367987499}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:07+00","p50":197.22465962499996,"p95":348.4032261016902,"p99":376.9117596460042}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:08+00","p50":120.9354125,"p95":240.57072478003738,"p99":275.8902450636096}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:09+00","p50":90.75474025,"p95":151.87280462502932,"p99":176.43251270116284}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:10+00","p50":116.773278,"p95":215.59808552930116,"p99":239.50228041238788}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:11+00","p50":97.52640099999999,"p95":195.7294668246715,"p99":264.2092767714319}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:12+00","p50":105.226463,"p95":192.82036787003804,"p99":216.07489888923166}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:13+00","p50":114.74559637499999,"p95":228.0915894401882,"p99":262.85628041826055}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:14+00","p50":77.801512,"p95":129.35629085421513,"p99":158.66147505569364}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:15+00","p50":84.58013687500001,"p95":160.30698757028998,"p99":174.41446144196416}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:16+00","p50":119.63030762500001,"p95":195.95116983887482,"p99":244.93045512882233}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:17+00","p50":84.94059075,"p95":144.44246646892853,"p99":196.845117347682}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:18+00","p50":95.36982275,"p95":159.31266557448865,"p99":183.6228256938827}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:19+00","p50":90.47498175,"p95":155.41825383879421,"p99":188.19613506901646}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:20+00","p50":154.866554625,"p95":275.1757444596352,"p99":320.10758409380554}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:21+00","p50":192.77049975,"p95":382.84073912148096,"p99":456.1590663652012}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:22+00","p50":172.684199,"p95":273.82849095250197,"p99":291.76658192851545}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:23+00","p50":77.349334125,"p95":164.6315947539091,"p99":217.74497885814333}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:24+00","p50":136.33481124999997,"p95":268.13097562302875,"p99":301.47499574422164}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:25+00","p50":111.7630275,"p95":208.5675628924989,"p99":246.8627640971558}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:26+00","p50":118.117255375,"p95":198.3906503164702,"p99":241.5595087475195}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:27+00","p50":84.57643475,"p95":169.85382740643405,"p99":232.07598728229524}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:28+00","p50":131.6372465,"p95":308.4863055378767,"p99":334.4099884776578}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:29+00","p50":195.06435299999998,"p95":345.84884438555525,"p99":408.5210983401051}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:30+00","p50":172.456024,"p95":299.1539311056702,"p99":361.0364903167076}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:31+00","p50":187.658683,"p95":328.58161559315585,"p99":437.94517637537194}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:32+00","p50":195.98734775,"p95":338.57030350411253,"p99":379.8266741465178}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:33+00","p50":67.911066875,"p95":151.74925286104792,"p99":214.33244606456924}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:34+00","p50":112.829188125,"p95":205.14536671550687,"p99":238.09364061297896}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:35+00","p50":118.69257975,"p95":211.63245172558194,"p99":230.34172917965387}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:36+00","p50":213.369049875,"p95":370.83680346451234,"p99":428.10711904918077}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:37+00","p50":171.915664,"p95":324.95000080962086,"p99":404.3369656036892}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:38+00","p50":94.7914515,"p95":178.3088233486929,"p99":203.75306774856568}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:39+00","p50":122.90329362499999,"p95":225.84800215345948,"p99":262.82832499983}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:40+00","p50":81.11874,"p95":146.975434843657,"p99":191.93503775610162}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:41+00","p50":77.06334225,"p95":172.44667276608277,"p99":233.7923764716797}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:42+00","p50":155.232269875,"p95":271.10978576326687,"p99":318.6952438457322}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:43+00","p50":95.44421399999999,"p95":161.0645313864994,"p99":198.1223385358639}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:44+00","p50":126.5642115,"p95":197.7667174037062,"p99":238.30415321547366}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:45+00","p50":224.1830465,"p95":357.49810249896115,"p99":397.73599232819583}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:46+00","p50":183.60946975000002,"p95":320.08706590544864,"p99":450.21918666022657}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:47+00","p50":198.033927375,"p95":329.51922549648947,"p99":374.69348892130114}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:48+00","p50":193.911937125,"p95":328.7849015878213,"p99":441.00926053497864}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:49+00","p50":182.81799275,"p95":328.8172208499136,"p99":387.04888753614046}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:50+00","p50":151.1433855,"p95":264.112782148159,"p99":310.14634120301156}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:51+00","p50":176.03035112499998,"p95":302.51729271481724,"p99":408.1084628826258}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:52+00","p50":214.949151,"p95":355.80821321688876,"p99":408.5701132212119}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:53+00","p50":183.38663587500002,"p95":297.250508663004,"p99":391.89707462702677}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:54+00","p50":120.04058987500001,"p95":203.94821230325422,"p99":240.99225547581196}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:55+00","p50":113.0905525,"p95":203.73093636490478,"p99":226.81731132236672}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:56+00","p50":155.13705,"p95":252.38153863984545,"p99":280.79138134726526}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:57+00","p50":115.20201500000002,"p95":214.57353205774103,"p99":251.4363846567497}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:58+00","p50":111.75302625,"p95":196.1925572291405,"p99":228.68420412065504}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:48:59+00","p50":167.9227565,"p95":286.7656297350428,"p99":309.4054061672588}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:00+00","p50":224.82924050000003,"p95":415.2437619785105,"p99":521.9959837867747}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:01+00","p50":141.41684099999998,"p95":260.5584576646181,"p99":319.0394320893049}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:02+00","p50":76.6780775,"p95":146.97312266774244,"p99":178.05853469419745}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:03+00","p50":181.739860625,"p95":308.6153627060426,"p99":352.76882108493544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:04+00","p50":94.38307237500001,"p95":161.2207859351411,"p99":176.27598044379496}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:05+00","p50":140.59592849999999,"p95":261.5541838513689,"p99":298.1815471367612}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:06+00","p50":167.475838625,"p95":310.9194381740066,"p99":354.23008671012616}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:07+00","p50":136.74798875,"p95":245.3962663980248,"p99":298.6517000871954}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:08+00","p50":94.704063875,"p95":173.34224821417865,"p99":202.8080740893066}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:09+00","p50":84.1258795,"p95":134.26196577129818,"p99":153.5552569835806}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:10+00","p50":119.949077,"p95":239.03918058059477,"p99":276.9778963696089}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:11+00","p50":77.22426025,"p95":192.5384956073041,"p99":289.3393825964851}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:12+00","p50":102.67467300000001,"p95":182.34886675243544,"p99":239.58761844983482}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:13+00","p50":150.819112,"p95":233.7346517537465,"p99":265.10101910305286}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:14+00","p50":90.555367,"p95":162.997830820631,"p99":219.04837551709034}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:15+00","p50":110.9382025,"p95":193.06792611819685,"p99":228.6568239175172}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:16+00","p50":162.57729949999998,"p95":267.1019260265363,"p99":327.8019226284208}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:17+00","p50":122.66068250000001,"p95":231.20068586499093,"p99":277.3198436298952}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:18+00","p50":130.81855099999999,"p95":259.10035311904,"p99":312.3832365103016}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:19+00","p50":164.85576275000003,"p95":289.5224881659961,"p99":328.18444492549423}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:20+00","p50":122.597790625,"p95":223.57372248219193,"p99":341.17334275113654}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:21+00","p50":92.08586125000001,"p95":265.4554385921103,"p99":315.4545488016095}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:22+00","p50":122.325744125,"p95":207.15518508237176,"p99":253.51440682408546}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:23+00","p50":124.0804405,"p95":208.5275148345012,"p99":250.09135234294865}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:24+00","p50":120.3082995,"p95":202.97296703789567,"p99":256.5924304861489}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:25+00","p50":93.203564,"p95":196.1170885300679,"p99":220.8877699315989}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:26+00","p50":129.740024,"p95":247.2575082027154,"p99":282.55843793993233}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:27+00","p50":167.099231,"p95":281.95474493365,"p99":331.7936293168857}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:28+00","p50":217.60455962499998,"p95":421.7247401407291,"p99":508.8911503200922}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:29+00","p50":208.02830887500002,"p95":426.15195239398247,"p99":512.820775347744}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:30+00","p50":127.14468675,"p95":238.14140667675363,"p99":288.02457367893504}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:31+00","p50":110.206042875,"p95":206.55647263575588,"p99":230.3438397485981}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:32+00","p50":146.81646999999998,"p95":264.03500699996164,"p99":317.5231723753433}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:33+00","p50":118.475527,"p95":217.3786848686085,"p99":303.1002206551065}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:34+00","p50":151.24810775,"p95":254.28615024905014,"p99":304.19432314852855}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:35+00","p50":139.93294925,"p95":235.42863928045207,"p99":276.86699031813765}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:36+00","p50":185.62641962499998,"p95":333.2691891589634,"p99":377.97228938201357}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:37+00","p50":92.817038625,"p95":198.14066718595106,"p99":260.0275987819607}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:38+00","p50":128.9801665,"p95":246.05489074892688,"p99":335.2701839859982}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:39+00","p50":169.57002125,"p95":285.0563691501379,"p99":339.24941110331724}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:40+00","p50":142.164916375,"p95":277.20559349273026,"p99":302.16137845514777}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:41+00","p50":122.179940625,"p95":211.79875760471558,"p99":260.66948875296305}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:42+00","p50":124.5509395,"p95":246.70578463223507,"p99":277.9563138941815}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:43+00","p50":95.05171375,"p95":185.21594304929064,"p99":232.606277288023}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:44+00","p50":113.73814875,"p95":197.42266716766906,"p99":245.20485068198798}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:45+00","p50":121.842034125,"p95":207.4706034665075,"p99":243.5389221920166}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:46+00","p50":145.223165,"p95":278.4664416509681,"p99":364.1667859398136}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:47+00","p50":214.81870600000002,"p95":405.14995807418813,"p99":511.2080387875444}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:48+00","p50":178.8440205,"p95":296.3816158212631,"p99":397.0811726249294}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:49+00","p50":145.45170275,"p95":265.83911376829934,"p99":294.33542210741996}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:50+00","p50":153.515479875,"p95":243.4696005607425,"p99":300.56652680123995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:51+00","p50":170.099188,"p95":292.67042164645727,"p99":365.5190332474301}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:52+00","p50":102.712198,"p95":197.0918927312405,"p99":266.6606889415236}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:53+00","p50":127.03653550000001,"p95":219.72031740333497,"p99":289.5474257745731}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:54+00","p50":88.312027625,"p95":157.61152782306738,"p99":203.33594262806605}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:55+00","p50":102.7510065,"p95":178.0299473281466,"p99":205.06328800445033}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:56+00","p50":177.268628,"p95":314.87519371836567,"p99":388.56729005886314}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:57+00","p50":176.7697255,"p95":318.8108334146526,"p99":392.3396298055153}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:58+00","p50":103.268483,"p95":247.5195731157218,"p99":300.77605382719446}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:49:59+00","p50":98.3880235,"p95":200.46048710468125,"p99":245.15390768143965}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:00+00","p50":146.90309474999998,"p95":258.3837046349163,"p99":307.3804545468025}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:01+00","p50":160.0529515,"p95":298.5677895518036,"p99":345.78969735470014}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:02+00","p50":150.128540875,"p95":329.02071908389263,"p99":531.6520238334601}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:03+00","p50":156.649613125,"p95":280.8244687110972,"p99":311.30939184340025}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:04+00","p50":99.58042225,"p95":205.33131582418952,"p99":248.3720814104178}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:05+00","p50":104.95082912500001,"p95":211.1258879247433,"p99":245.4109877830725}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:06+00","p50":100.86131925,"p95":177.74532394839514,"p99":203.38544704085706}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:07+00","p50":166.717813625,"p95":301.4464004139329,"p99":422.27069196440914}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:08+00","p50":183.122375,"p95":320.2672457518171,"p99":337.5414996871753}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:09+00","p50":97.1126915,"p95":168.9100202090468,"p99":221.52435799082065}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:10+00","p50":157.042794125,"p95":286.1850065083168,"p99":365.80392996135953}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:11+00","p50":175.701706875,"p95":293.9338822561189,"p99":330.85295726367525}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:12+00","p50":88.737670125,"p95":168.60583470118254,"p99":203.13561862069727}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:13+00","p50":133.26832124999999,"p95":219.04042586153864,"p99":262.71531470463657}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:14+00","p50":129.042876,"p95":224.137657865206,"p99":267.8677011634698}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:15+00","p50":130.056198875,"p95":272.8999871089746,"p99":297.60733383080384}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:16+00","p50":129.08648374999999,"p95":295.1839315693486,"p99":369.9986646130562}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:17+00","p50":148.86776500000002,"p95":236.155151247262,"p99":279.66531103663135}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:18+00","p50":101.831662,"p95":214.4762249565202,"p99":244.41268101225162}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:19+00","p50":123.8757715,"p95":205.94489188140608,"p99":275.1826857816892}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:20+00","p50":120.77973237500001,"p95":222.15841217891693,"p99":303.6909217581429}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:21+00","p50":75.2721225,"p95":135.00010428689956,"p99":152.01190308586501}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:22+00","p50":142.515475,"p95":237.75577908824337,"p99":255.26098916444016}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:23+00","p50":229.05779,"p95":406.399523632061,"p99":463.79293193337446}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:24+00","p50":183.48794475,"p95":301.6164604326537,"p99":348.5588254164195}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:25+00","p50":146.28048875,"p95":220.24426210923073,"p99":265.43960687419434}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:26+00","p50":112.3129395,"p95":206.35636717751538,"p99":251.96538080867958}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:27+00","p50":116.95516225,"p95":262.5124719971018,"p99":353.72607107038306}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:28+00","p50":122.60860525000001,"p95":231.62751081742525,"p99":292.3822842529438}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:29+00","p50":114.589837,"p95":194.2496299961059,"p99":264.8560597473068}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:30+00","p50":79.548655,"p95":142.62886833260654,"p99":179.5888004395504}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:31+00","p50":116.30000162500001,"p95":222.71003528821404,"p99":257.5645911491313}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:32+00","p50":90.505556125,"p95":163.7167971977871,"p99":212.71995809886624}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:33+00","p50":110.53585724999999,"p95":197.26180029928778,"p99":241.94576236033726}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:34+00","p50":119.95915175,"p95":207.5164353306854,"p99":232.09581205182647}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:35+00","p50":168.667465875,"p95":328.61260283776,"p99":402.5135138609762}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:36+00","p50":180.89536225,"p95":353.2266079673419,"p99":405.60131409118753}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:37+00","p50":95.97314600000001,"p95":201.23708436596726,"p99":245.47708789807032}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:38+00","p50":110.021550875,"p95":192.7480758854617,"p99":265.0225016271269}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:39+00","p50":183.5119125,"p95":364.8602129200367,"p99":403.3772781636033}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:40+00","p50":127.330326,"p95":229.20515150027168,"p99":266.8002532100673}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:41+00","p50":118.674792,"p95":206.78046656192885,"p99":229.86783207067873}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:42+00","p50":180.7894175,"p95":302.89422436608453,"p99":359.21675440356256}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:43+00","p50":143.47751,"p95":289.9611552879882,"p99":352.50293727325965}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:44+00","p50":130.887541,"p95":216.0389818101513,"p99":271.3052527049067}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:45+00","p50":119.14032875,"p95":196.9254226763699,"p99":225.5676313746543}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:46+00","p50":118.47727925,"p95":219.72585911291122,"p99":253.31050452684022}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:47+00","p50":157.538616875,"p95":296.2515551230171,"p99":455.93793386845635}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:48+00","p50":210.165097875,"p95":355.49922648230694,"p99":424.17380526396727}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:49+00","p50":119.03616149999999,"p95":236.0419699379651,"p99":288.5680091757014}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:50+00","p50":131.827156,"p95":233.77617902404978,"p99":274.03361541597934}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:51+00","p50":117.56609424999999,"p95":219.11308931945322,"p99":267.5331223156605}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:52+00","p50":100.389098,"p95":183.9928992499466,"p99":241.56227272518922}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:53+00","p50":171.8630655,"p95":290.7261397174173,"p99":344.7048438689754}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:54+00","p50":99.35622962500001,"p95":233.54207504769414,"p99":254.59255876947594}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:55+00","p50":121.206158125,"p95":212.3466707781859,"p99":250.96570182067228}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:56+00","p50":161.7219235,"p95":307.6132760159545,"p99":391.686095346138}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:57+00","p50":118.865662,"p95":195.68221622017933,"p99":267.5168934990549}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:58+00","p50":134.26175,"p95":231.75871150604033,"p99":266.3252995473752}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:50:59+00","p50":123.470201,"p95":237.86671347830728,"p99":270.27631510565567}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:00+00","p50":154.00722525,"p95":281.5003241502898,"p99":389.9820110855813}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:01+00","p50":129.7291185,"p95":213.47464407658876,"p99":262.62097807222915}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:02+00","p50":93.06392349999999,"p95":160.7843555688497,"p99":187.47902267387795}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:03+00","p50":151.83697562499998,"p95":280.21147077789027,"p99":356.24208264526743}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:04+00","p50":90.345341125,"p95":181.82349715326757,"p99":275.8178891571078}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:05+00","p50":104.729288,"p95":188.66138582846258,"p99":240.90825274350166}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:06+00","p50":160.840256375,"p95":309.89247545559795,"p99":378.0344772004645}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:07+00","p50":85.229369375,"p95":161.55322203660756,"p99":192.91834185984303}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:08+00","p50":143.271519125,"p95":245.0577006863131,"p99":309.7107219738107}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:09+00","p50":187.563281125,"p95":325.6732923283192,"p99":378.05493228439906}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:10+00","p50":107.2644335,"p95":225.85179870588146,"p99":249.48406465885114}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:11+00","p50":106.650567375,"p95":211.34638745335698,"p99":233.4311838638437}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:12+00","p50":114.00769700000001,"p95":203.3047071122192,"p99":287.2812678587418}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:13+00","p50":135.55943424999998,"p95":215.80626879198135,"p99":234.85129264763975}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:14+00","p50":100.503893,"p95":183.35932405695277,"p99":223.99793029223085}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:15+00","p50":94.84304625,"p95":206.92393512529674,"p99":329.1126737521343}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:16+00","p50":120.885145375,"p95":206.99291664177753,"p99":255.1351456957145}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:17+00","p50":116.53029362500001,"p95":186.629567002656,"p99":209.09682495160223}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:18+00","p50":82.964399875,"p95":144.27417768103106,"p99":180.5137818067434}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:19+00","p50":120.7923115,"p95":206.1640771978742,"p99":279.53199130701995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:20+00","p50":179.56962199999998,"p95":325.45724340495013,"p99":346.95102908232684}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:21+00","p50":164.273868,"p95":307.5376546449261,"p99":390.1122033200645}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:22+00","p50":111.662962,"p95":208.1805942513585,"p99":236.2707117055986}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:23+00","p50":117.1332045,"p95":181.96947758981182,"p99":222.20426819460153}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:24+00","p50":124.3130085,"p95":267.1455348637066,"p99":327.39941019806673}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:25+00","p50":195.46493099999998,"p95":365.1929777199172,"p99":408.03205740565016}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:26+00","p50":196.8258355,"p95":411.72842267801144,"p99":535.1871792073679}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:27+00","p50":154.86725762499998,"p95":255.20607415540132,"p99":317.75992903017476}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:28+00","p50":152.896679,"p95":293.3784397256216,"p99":304.6741785511951}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:29+00","p50":155.0317435,"p95":263.3453652043408,"p99":307.2539843087988}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:30+00","p50":109.07427725,"p95":182.0128916178384,"p99":214.65147290107154}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:31+00","p50":74.95376225,"p95":151.85466803796317,"p99":173.29790198296402}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:32+00","p50":126.253039375,"p95":259.35468004291187,"p99":299.97295851230257}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:33+00","p50":106.782730375,"p95":204.73639574529153,"p99":230.2037609356697}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:34+00","p50":77.592853,"p95":149.073914495162,"p99":181.25237094677854}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:35+00","p50":117.59886125,"p95":213.06034566206716,"p99":249.66882168813515}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:36+00","p50":169.20732412499999,"p95":296.83574156857594,"p99":390.65112805267853}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:37+00","p50":83.40732025,"p95":182.429370852191,"p99":254.23594313975406}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:38+00","p50":146.371172,"p95":260.2307077156852,"p99":293.6660845240202}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:39+00","p50":136.6599085,"p95":266.9424442772744,"p99":326.5919648148446}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:40+00","p50":113.744263375,"p95":192.63153096754257,"p99":254.26214025607467}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:41+00","p50":85.820059875,"p95":226.23223968883954,"p99":270.71967096236847}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:42+00","p50":149.4080565,"p95":280.8059415548611,"p99":313.6195509385619}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:43+00","p50":152.051569,"p95":248.91104037124467,"p99":311.54247824412727}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:44+00","p50":130.52296074999998,"p95":232.36452114235448,"p99":274.7962563942003}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:45+00","p50":70.1395675,"p95":123.35066589601672,"p99":156.60205119753027}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:46+00","p50":96.625868125,"p95":183.39243869545888,"p99":212.7432642164514}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:47+00","p50":117.09850825000001,"p95":202.54137917712927,"p99":250.13275217692564}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:48+00","p50":168.49530425,"p95":319.15811539331565,"p99":388.41429543138673}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:49+00","p50":140.413608625,"p95":253.3503788783474,"p99":318.52861722436575}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:50+00","p50":91.66550575,"p95":212.2856313274133,"p99":318.068791436717}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:51+00","p50":81.49358475,"p95":172.40554114372063,"p99":208.20557141838836}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:52+00","p50":155.1393645,"p95":277.5231534315592,"p99":397.3598628490477}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:53+00","p50":132.7417735,"p95":239.64003198251243,"p99":290.4959346638098}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:54+00","p50":108.553943375,"p95":247.2682456126909,"p99":358.86615377725747}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:55+00","p50":101.3413005,"p95":188.21243710128286,"p99":218.86255514750673}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:56+00","p50":118.961154,"p95":198.20017657363,"p99":250.03312448099183}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:57+00","p50":99.79244425,"p95":181.5631235959376,"p99":202.5538525555277}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:58+00","p50":104.33326325,"p95":181.79779599586146,"p99":238.01424752856826}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:51:59+00","p50":168.283834125,"p95":278.8986466224676,"p99":380.633144924258}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:00+00","p50":126.13562825,"p95":282.45784046678546,"p99":346.628474314743}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:01+00","p50":111.53798425,"p95":185.94839395953275,"p99":245.19764505337906}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:02+00","p50":86.81748400000001,"p95":239.10644267528843,"p99":325.50113612338544}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:03+00","p50":87.97501299999999,"p95":165.06821366291106,"p99":219.27582761927175}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:04+00","p50":124.93196599999999,"p95":237.31974525656273,"p99":262.02568710580897}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:05+00","p50":123.711987125,"p95":228.4094063168304,"p99":274.8666600792418}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:06+00","p50":98.072877125,"p95":230.09177989653458,"p99":266.873102909143}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:07+00","p50":132.6589735,"p95":236.75743873051155,"p99":267.6254853911257}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:08+00","p50":144.38418925,"p95":265.3189291132889,"p99":327.9265653837242}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:09+00","p50":146.52195899999998,"p95":266.36128933518916,"p99":293.11751403433465}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:10+00","p50":137.33270249999998,"p95":245.85317999357903,"p99":283.8972081700087}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:11+00","p50":146.81632675000003,"p95":237.18059145020538,"p99":269.292118008702}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:12+00","p50":156.39164399999999,"p95":284.1061618011153,"p99":328.3944570183432}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:13+00","p50":150.806582625,"p95":297.43026819839884,"p99":473.9097886909747}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:14+00","p50":320.29789775,"p95":540.524811185647,"p99":605.4852741778469}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:15+00","p50":226.94066800000002,"p95":446.3928976948423,"p99":639.9215551901208}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:16+00","p50":166.71327187500003,"p95":298.10652295369346,"p99":386.7117500373597}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:17+00","p50":111.920357,"p95":215.35585461465584,"p99":266.88880515396215}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:18+00","p50":138.917835,"p95":241.31910431464573,"p99":325.2046773614538}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:19+00","p50":90.65563725000001,"p95":192.52264549704392,"p99":235.3077248107915}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:20+00","p50":115.35081125,"p95":189.27525507185555,"p99":217.1258047511549}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:21+00","p50":126.56002212499999,"p95":244.95850731101646,"p99":290.1303977500341}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:22+00","p50":144.029857875,"p95":241.73705388084758,"p99":309.7494202399132}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:23+00","p50":119.2352835,"p95":215.15636765691184,"p99":280.1774571783829}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:24+00","p50":103.5776725,"p95":195.32270777871705,"p99":249.45473508719635}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:25+00","p50":168.769308,"p95":300.1240431842201,"p99":342.4066091013217}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:26+00","p50":147.0156745,"p95":255.096405456769,"p99":364.603036187634}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:27+00","p50":143.19084487499998,"p95":266.08419577184577,"p99":358.64764975809004}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:28+00","p50":127.6609255,"p95":225.98080810222388,"p99":307.42457842840577}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:29+00","p50":174.039226375,"p95":290.6908228342428,"p99":323.44279019388773}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:30+00","p50":124.96149450000001,"p95":216.48245087079704,"p99":257.2035163484175}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:31+00","p50":163.81525675,"p95":263.3379526861093,"p99":302.96073392539694}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:32+00","p50":84.54048650000001,"p95":162.58883438706445,"p99":194.2747216799221}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:33+00","p50":108.98936425000001,"p95":198.51968096344686,"p99":227.04949217227076}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:34+00","p50":116.52611175000001,"p95":219.00267330736543,"p99":303.50417787607}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:35+00","p50":90.88516075,"p95":165.65721198175717,"p99":189.32268472167587}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:36+00","p50":165.93207800000002,"p95":276.54739244908046,"p99":326.96263984127427}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:37+00","p50":165.578486875,"p95":304.3416248746096,"p99":369.3812872410314}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:38+00","p50":165.5636275,"p95":318.6784747296823,"p99":442.54185876448537}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:39+00","p50":131.9669295,"p95":221.83727565478355,"p99":239.29759279992913}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:40+00","p50":155.30381699999998,"p95":270.62655694743216,"p99":332.57354560597724}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:41+00","p50":98.343100875,"p95":219.31609260253757,"p99":267.8947548812242}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:42+00","p50":103.235269625,"p95":190.48561052913757,"p99":268.8464940032773}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:43+00","p50":83.594595125,"p95":180.22228815123628,"p99":221.74870052820037}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:44+00","p50":119.44187600000001,"p95":198.73267990121602,"p99":252.53365634926223}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:45+00","p50":123.39206225,"p95":206.86911124046028,"p99":254.14775884353995}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:46+00","p50":175.4323175,"p95":287.1280740171566,"p99":320.8596715998764}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:47+00","p50":126.95785300000001,"p95":236.2430289919333,"p99":330.043723352679}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:48+00","p50":110.307762,"p95":196.52463128223883,"p99":261.20366756550357}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:49+00","p50":100.94030475,"p95":192.28229499746178,"p99":243.81540265726136}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:50+00","p50":93.03156437499999,"p95":187.31681875481374,"p99":219.2329354429028}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:51+00","p50":208.5059195,"p95":407.247762829814,"p99":471.3170308589935}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:52+00","p50":197.22009450000002,"p95":407.43791762805284,"p99":490.9483895758133}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:53+00","p50":85.005221,"p95":145.34607068533157,"p99":168.4000683345871}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:54+00","p50":183.38177275,"p95":334.41698259031716,"p99":417.8717258277242}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:55+00","p50":117.6200465,"p95":185.64741003220416,"p99":222.99019337348605}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:56+00","p50":135.2481975,"p95":252.49362606021262,"p99":282.5067917837944}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:57+00","p50":168.303939875,"p95":305.80189086629673,"p99":351.0019866665633}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:58+00","p50":134.09892,"p95":250.76849940422846,"p99":277.80129343396186}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:52:59+00","p50":163.6162775,"p95":294.31222897847937,"p99":387.4562411678009}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:00+00","p50":131.720675,"p95":245.93711948453736,"p99":302.98511350343324}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:01+00","p50":71.58291,"p95":145.77722574045563,"p99":183.94147398847724}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:02+00","p50":109.892257,"p95":182.71514545124734,"p99":227.5126410018134}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:03+00","p50":137.67564462500002,"p95":240.02062077103,"p99":274.04224269028236}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:04+00","p50":117.93918099999999,"p95":209.95260282921754,"p99":245.2308188142228}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:05+00","p50":123.99188325,"p95":211.05377992915487,"p99":258.05353511692044}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:06+00","p50":103.10748749999999,"p95":170.89863492273997,"p99":227.08697831586647}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:07+00","p50":199.341422125,"p95":333.83150373845814,"p99":395.2688375241308}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:08+00","p50":127.64809575,"p95":236.56410585796786,"p99":276.6294723583283}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:09+00","p50":217.855234,"p95":429.51657032219487,"p99":602.5529060169549}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:10+00","p50":171.59732874999997,"p95":420.97465439828125,"p99":495.6398960492558}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:11+00","p50":113.174409,"p95":182.79668626626585,"p99":218.1431058512287}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:12+00","p50":117.655407625,"p95":195.99780262472862,"p99":218.68925320661688}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:13+00","p50":84.919244,"p95":181.77757636352385,"p99":209.5801243482208}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:14+00","p50":96.19630825,"p95":198.4323302735982,"p99":230.9505951688986}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:15+00","p50":102.236762,"p95":164.03049672261045,"p99":195.12487519128226}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:16+00","p50":121.786966,"p95":267.8807147272338,"p99":348.1064134802561}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:17+00","p50":97.20524525,"p95":178.1615742626803,"p99":210.4522790867605}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:18+00","p50":147.44651900000002,"p95":257.7905376768967,"p99":311.13195824380585}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:19+00","p50":143.87416149999999,"p95":255.4682522036294,"p99":288.3889778533177}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:20+00","p50":117.091132125,"p95":210.62780327862228,"p99":231.355456420043}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:21+00","p50":101.397438,"p95":167.26811514437722,"p99":188.12762321937706}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:22+00","p50":119.577436,"p95":194.78064204113508,"p99":254.49614983953188}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:23+00","p50":120.2645035,"p95":207.5051592612064,"p99":255.90936522483037}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:24+00","p50":157.799546875,"p95":283.30800800036985,"p99":325.0002693955023}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:25+00","p50":116.344294875,"p95":204.78986157396614,"p99":230.40916692351126}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:26+00","p50":128.23026049999999,"p95":236.27942650394237,"p99":273.70131086306026}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:27+00","p50":110.92290575,"p95":216.3857756006696,"p99":237.8508319802327}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:28+00","p50":88.307181,"p95":157.88738628039144,"p99":182.00874847536468}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:29+00","p50":115.50933875000001,"p95":211.56972721835993,"p99":261.7515177608833}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:30+00","p50":107.74433675,"p95":228.3813131273538,"p99":361.4905189335232}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:31+00","p50":114.502713625,"p95":269.599796593049,"p99":311.7073022712738}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:32+00","p50":89.85264175,"p95":154.4912205238502,"p99":196.22627979261873}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:33+00","p50":137.62530075,"p95":259.7832489259487,"p99":291.9390314182644}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:34+00","p50":285.922503,"p95":442.8536779012476,"p99":688.8098935577111}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:35+00","p50":193.871565,"p95":356.32478476509465,"p99":424.53858306423473}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:36+00","p50":68.16457625,"p95":197.01251420711935,"p99":252.15860704356717}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:37+00","p50":92.05566637500002,"p95":162.30829423921853,"p99":203.07178867003418}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:38+00","p50":155.6708565,"p95":266.24145529667027,"p99":302.5513511829038}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:39+00","p50":154.714533375,"p95":291.40366085526637,"p99":322.55325491924594}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:40+00","p50":102.219995,"p95":173.65903901474667,"p99":193.9816556112261}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:41+00","p50":138.9728095,"p95":225.98884852322985,"p99":273.4696420383363}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:42+00","p50":107.745142,"p95":199.84412234244144,"p99":243.3159855037446}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:43+00","p50":150.103519,"p95":249.56462417661282,"p99":275.9483564173875}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:44+00","p50":109.52588575000001,"p95":195.94282094673292,"p99":241.3183397635279}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:45+00","p50":161.54639775,"p95":291.923245494941,"p99":348.51720574694394}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:46+00","p50":112.03695675,"p95":242.6844271712753,"p99":316.76976274487635}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:47+00","p50":196.86149749999998,"p95":318.35349088719084,"p99":372.8232505989418}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:48+00","p50":140.887495875,"p95":294.5654741458619,"p99":352.45711676727797}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:49+00","p50":126.93650274999999,"p95":212.5110348324349,"p99":231.78316493113996}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:50+00","p50":93.01092949999999,"p95":220.27748652256926,"p99":259.224393996696}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:51+00","p50":221.33150799999999,"p95":357.88941950179503,"p99":418.6976849957285}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:52+00","p50":193.90783512499996,"p95":357.08542957238694,"p99":469.6985823343909}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:53+00","p50":87.34625299999999,"p95":142.78101826847458,"p99":196.01997949695493}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:54+00","p50":98.849050375,"p95":159.2666017445391,"p99":185.8293930091355}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:55+00","p50":131.2583675,"p95":209.13301270854737,"p99":234.4567246685834}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:56+00","p50":164.010242,"p95":251.43579632584905,"p99":332.33864903721}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:57+00","p50":117.3083135,"p95":213.73942415053273,"p99":304.75950912980267}, + {"metric_name":"login_ui_init_login_duration","timestamp":"2025-02-25 14:53:58+00","p50":73.22265,"p95":122.60413838599837,"p99":136.7482057540102}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:23:59+00","p50":109.31481525000001,"p95":154.55452349393272,"p99":171.62406084524153}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:00+00","p50":108.55757700000001,"p95":138.64728572800004,"p99":154.35811999713323}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:01+00","p50":97.042144375,"p95":123.22991794083792,"p99":129.40205248136854}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:02+00","p50":97.00180925,"p95":134.2261342757446,"p99":145.2694474319651}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:03+00","p50":90.624956,"p95":126.907699397789,"p99":137.20140117649078}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:04+00","p50":97.97581325,"p95":142.00510327856088,"p99":160.34901856955906}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:05+00","p50":84.426014125,"p95":109.68051860609657,"p99":120.88638988336753}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:06+00","p50":101.417083,"p95":260.57809664189347,"p99":273.8161387157719}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:07+00","p50":93.940038125,"p95":121.30382673902488,"p99":130.0368920216451}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:08+00","p50":107.14581475,"p95":150.22358242681503,"p99":194.96894837445544}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:09+00","p50":96.56016825,"p95":135.6294432327325,"p99":145.37852832510663}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:10+00","p50":98.453850125,"p95":135.64106029146498,"p99":146.93523236865448}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:11+00","p50":96.422657125,"p95":132.176772258704,"p99":141.31724321748044}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:12+00","p50":82.240924,"p95":128.75242463836764,"p99":139.07629813593246}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:13+00","p50":85.94216375,"p95":122.789760730711,"p99":145.5798287668419}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:14+00","p50":91.084237125,"p95":120.47630946038913,"p99":129.68135244055676}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:15+00","p50":99.33266775000001,"p95":134.04981968679428,"p99":150.7337107786026}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:16+00","p50":110.213677125,"p95":229.33018690412368,"p99":254.47898817757297}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:17+00","p50":142.644020125,"p95":301.00222430050593,"p99":392.5541812414815}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:18+00","p50":153.573145,"p95":583.90950208447,"p99":738.8167869863802}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:19+00","p50":295.32104175,"p95":662.9313481183731,"p99":674.0107326062496}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:20+00","p50":519.16929325,"p95":643.8729357571335,"p99":1158.3483905691976}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:21+00","p50":568.56631825,"p95":630.1640343562138,"p99":661.8426647354489}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:22+00","p50":90.701657625,"p95":737.0587015206803,"p99":803.0726613079296}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:23+00","p50":112.88001324999999,"p95":636.0175031444794,"p99":650.0548105257914}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:24+00","p50":392.5179645,"p95":672.7162635679597,"p99":1091.2747110403}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:25+00","p50":114.082023125,"p95":587.3688026041335,"p99":606.5847784039886}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:26+00","p50":154.803429,"p95":619.6296934502325,"p99":668.8449814461961}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:27+00","p50":170.84798625,"p95":601.0918202072431,"p99":1116.3241579921098}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:28+00","p50":476.1491885,"p95":571.8759185447418,"p99":637.9821964281016}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:29+00","p50":485.96173387500005,"p95":550.9454050453671,"p99":604.972164511359}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:30+00","p50":522.1323768750001,"p95":588.385146508448,"p99":601.4335976871795}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:31+00","p50":372.64576962499996,"p95":615.1019359239099,"p99":630.4445595040999}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:32+00","p50":455.1879155,"p95":688.4440564632664,"p99":1186.2473438661495}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:33+00","p50":512.30537325,"p95":616.9509460128509,"p99":994.8566648591404}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:34+00","p50":436.87954987499995,"p95":663.1757735673757,"p99":1007.0286015171778}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:35+00","p50":90.70940337500002,"p95":661.1728569375961,"p99":942.5292599982727}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:36+00","p50":99.15016025,"p95":641.7848848824134,"p99":987.662908750966}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:37+00","p50":356.73008175,"p95":578.8645212039597,"p99":625.4316185402794}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:38+00","p50":205.536585,"p95":476.72402679023014,"p99":749.3904046382847}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:39+00","p50":398.22751600000004,"p95":472.64473172909356,"p99":792.5024176856595}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:40+00","p50":343.430345,"p95":495.1540912528609,"p99":871.0015097620516}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:41+00","p50":282.0802095,"p95":371.7838904712502,"p99":471.6887385451193}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:42+00","p50":301.994558,"p95":398.70560368940926,"p99":648.2878693106127}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:43+00","p50":350.42316774999995,"p95":417.31846196109535,"p99":453.0966475133228}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:44+00","p50":319.95318699999996,"p95":402.8307393414954,"p99":694.6087773167408}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:45+00","p50":322.58584225,"p95":398.1782243228741,"p99":596.6982763843326}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:46+00","p50":222.471095,"p95":517.2233907765482,"p99":806.7682427098858}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:47+00","p50":349.073912,"p95":540.5477308748589,"p99":892.4337269745083}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:48+00","p50":271.08276137499996,"p95":525.1984549786908,"p99":836.4152346160695}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:49+00","p50":299.49430812500003,"p95":422.48586700498623,"p99":458.9668971491797}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:50+00","p50":314.470497,"p95":387.22404108095236,"p99":576.7742278460888}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:51+00","p50":355.9981165,"p95":529.7834909034377,"p99":557.0609289183259}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:52+00","p50":181.40220950000003,"p95":453.8391078087528,"p99":461.5098329236059}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:53+00","p50":382.40290224999995,"p95":504.42110377608856,"p99":829.5782895816739}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:54+00","p50":294.64877475,"p95":462.2199622280524,"p99":692.6990517589093}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:55+00","p50":342.2092955,"p95":460.63826081108164,"p99":712.1092915929041}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:56+00","p50":381.69132975,"p95":427.9231688933774,"p99":536.4544763303418}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:57+00","p50":325.08059149999997,"p95":417.8733222703052,"p99":429.6819178217039}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:58+00","p50":257.135579,"p95":360.1802899619409,"p99":526.9504352320004}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:24:59+00","p50":286.28916425,"p95":361.4900207049391,"p99":515.2231766924925}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:00+00","p50":328.27501175,"p95":424.67096746628846,"p99":595.7954600015242}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:01+00","p50":314.914217,"p95":381.36479475844334,"p99":584.3127821415882}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:02+00","p50":324.076719875,"p95":416.90090067713624,"p99":597.9131760957471}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:03+00","p50":349.404782,"p95":474.7604815090523,"p99":685.0525788379249}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:04+00","p50":301.123802125,"p95":383.396410451621,"p99":405.41518389371015}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:05+00","p50":310.84967825,"p95":356.6716713831876,"p99":502.9027298169475}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:06+00","p50":325.05020375000004,"p95":460.0866143799211,"p99":570.5547340716067}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:07+00","p50":327.11590625,"p95":428.9635791250935,"p99":821.5910969551696}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:08+00","p50":421.88074674999996,"p95":493.9501436070838,"p99":564.5957618604288}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:09+00","p50":327.416399,"p95":428.4357147154179,"p99":771.524265211853}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:10+00","p50":274.419486375,"p95":441.0547050040045,"p99":459.06707104448276}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:11+00","p50":290.332530375,"p95":413.3220354888969,"p99":429.5895169247761}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:12+00","p50":275.100573375,"p95":419.94213993428434,"p99":473.43888495532036}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:13+00","p50":206.605547875,"p95":521.5125274839622,"p99":579.9140535798376}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:14+00","p50":381.6960395,"p95":503.37312462919374,"p99":791.0229590778491}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:15+00","p50":238.191700125,"p95":453.78385024592166,"p99":742.9149374380497}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:16+00","p50":136.650386875,"p95":492.80011221323036,"p99":815.0189173900511}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:17+00","p50":168.52574025,"p95":565.2720398068292,"p99":582.559903581808}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:18+00","p50":99.293683,"p95":712.1552034780095,"p99":1049.7506560666407}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:19+00","p50":74.9825345,"p95":833.0543036648455,"p99":989.3080257172184}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:20+00","p50":634.62676425,"p95":745.0865104454515,"p99":794.8671877924969}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:21+00","p50":577.3017175,"p95":694.8949612588336,"p99":712.038904883629}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:22+00","p50":481.787727,"p95":576.5188672781818,"p99":1085.5172199633082}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:23+00","p50":496.882085125,"p95":645.6561938021638,"p99":661.8306168333681}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:24+00","p50":76.303494,"p95":651.3446294204746,"p99":1049.2403308929127}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:25+00","p50":85.29985125,"p95":693.0575651677256,"p99":751.8067983734475}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:26+00","p50":83.78057925,"p95":747.574227383256,"p99":764.007034401702}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:27+00","p50":73.32362562499999,"p95":866.7760986507016,"p99":886.7007439117122}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:28+00","p50":278.913117875,"p95":806.1389870018271,"p99":1397.9924164147617}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:29+00","p50":111.78796137500001,"p95":838.3875741505765,"p99":1493.5750383296465}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:30+00","p50":708.281515,"p95":800.8660953024171,"p99":878.182707969471}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:31+00","p50":683.54794225,"p95":768.296300207732,"p99":1358.1964765058337}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:32+00","p50":710.374452375,"p95":805.1618837123597,"p99":1485.139700402976}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:33+00","p50":724.31706875,"p95":825.343924367875,"p99":1406.4376497668784}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:34+00","p50":726.2675987499999,"p95":884.0710607909591,"p99":1490.2753964487306}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:35+00","p50":109.890361625,"p95":874.4415897974745,"p99":891.9998961224616}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:36+00","p50":117.93634975,"p95":764.7478113918214,"p99":997.2901433636417}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:37+00","p50":72.57178325000001,"p95":1069.3798917623299,"p99":1190.9516234011862}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:38+00","p50":284.1733515,"p95":909.712190934455,"p99":947.7232591773023}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:39+00","p50":665.242015,"p95":870.3322596991449,"p99":1393.4111614628239}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:40+00","p50":503.57255,"p95":731.5069151026777,"p99":1209.343395166429}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:41+00","p50":109.502630875,"p95":704.7464490457003,"p99":709.570573319685}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:42+00","p50":602.53707,"p95":703.5683162750544,"p99":1262.1450156061273}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:43+00","p50":106.3891795,"p95":763.7859262719534,"p99":795.8717291013776}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:44+00","p50":111.4219705,"p95":717.6155453083556,"p99":749.340125447754}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:45+00","p50":609.3002322499999,"p95":760.8231321157707,"p99":1265.5796221658475}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:46+00","p50":604.7427475,"p95":784.386918238086,"p99":801.1440397261896}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:47+00","p50":533.97834125,"p95":634.0788463382303,"p99":1044.0398414862505}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:48+00","p50":171.416556375,"p95":702.0190137528176,"p99":1091.0705565179478}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:49+00","p50":661.4410855,"p95":755.2819769627883,"p99":763.9670211372265}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:50+00","p50":637.445357,"p95":734.3641043901413,"p99":776.8865326196683}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:51+00","p50":703.003802875,"p95":755.1927574672535,"p99":1327.120874227663}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:52+00","p50":138.0125885,"p95":756.1563954694016,"p99":1321.6494612479673}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:53+00","p50":126.196268,"p95":636.6694754173477,"p99":658.4620305417385}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:54+00","p50":138.17920212500002,"p95":623.5526520108621,"p99":1024.449381893333}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:55+00","p50":516.5169985,"p95":678.438787777709,"p99":1051.6861384170122}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:56+00","p50":685.046824,"p95":819.5570226161875,"p99":967.0053906356496}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:57+00","p50":510.146699375,"p95":745.6093420380134,"p99":1119.831744010305}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:58+00","p50":334.13669125,"p95":573.4472110342676,"p99":839.6189804343328}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:25:59+00","p50":298.82428525,"p95":358.24885109682486,"p99":554.2020926412106}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:00+00","p50":303.37607075,"p95":371.1084543289154,"p99":557.2149954344696}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:01+00","p50":334.2291,"p95":391.1643560256002,"p99":589.2727821883144}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:02+00","p50":276.9628415,"p95":351.20628986003425,"p99":618.9385668886175}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:03+00","p50":294.151058,"p95":464.47859935265683,"p99":475.1976795982342}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:04+00","p50":296.535407625,"p95":431.4931298290636,"p99":615.0044062695802}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:05+00","p50":302.029727,"p95":349.567741750622,"p99":508.4987073040342}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:06+00","p50":331.80417524999996,"p95":436.1539976350708,"p99":683.606131430214}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:07+00","p50":288.558766,"p95":392.96240698623564,"p99":536.5816777286195}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:08+00","p50":261.7724905,"p95":446.36652137352326,"p99":633.0469677051208}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:09+00","p50":201.16775725,"p95":486.66220026979255,"p99":499.7222875604296}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:10+00","p50":269.15578975,"p95":663.2524479404402,"p99":1070.612847433426}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:11+00","p50":571.3837263749999,"p95":651.7913210560391,"p99":1062.2597741716527}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:12+00","p50":108.475210375,"p95":549.1157049990305,"p99":939.324118679212}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:13+00","p50":514.5353525,"p95":633.2618554423149,"p99":642.567389617383}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:14+00","p50":552.64949525,"p95":726.0518601302927,"p99":1106.669356484809}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:15+00","p50":97.22883525,"p95":773.4029241632686,"p99":792.7198979583278}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:16+00","p50":76.209307,"p95":768.9567476320237,"p99":1329.1150077244893}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:17+00","p50":119.63799325000001,"p95":645.0540559136257,"p99":1149.6124788131551}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:18+00","p50":103.805427,"p95":755.2290762902447,"p99":780.7530385672083}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:19+00","p50":435.88685975,"p95":846.8390478671441,"p99":937.5019653829174}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:20+00","p50":680.7908935,"p95":814.0193439077196,"p99":926.1912245397067}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:21+00","p50":125.564724125,"p95":828.4046639811004,"p99":839.8737331290162}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:22+00","p50":554.2134707499999,"p95":833.9283846025248,"p99":1055.0322426671696}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:23+00","p50":603.780134,"p95":693.6877545737295,"p99":1111.9294112593823}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:24+00","p50":103.84965675000001,"p95":690.9574892974596,"p99":705.2779868713399}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:25+00","p50":225.79344100000003,"p95":742.0420279412167,"p99":1339.024918603162}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:26+00","p50":662.026627375,"p95":745.6842326301615,"p99":786.777857077372}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:27+00","p50":346.142374,"p95":714.506023285614,"p99":1262.3551606746519}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:28+00","p50":520.4640603749999,"p95":682.3646507255502,"p99":709.3648709923222}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:29+00","p50":107.95026425,"p95":542.187130415435,"p99":560.2418908862762}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:30+00","p50":79.80811675000001,"p95":741.1588434503177,"p99":766.0455162520843}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:31+00","p50":425.52645762500003,"p95":693.2495858676024,"p99":737.464574454315}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:32+00","p50":558.7392500000001,"p95":712.2336600888394,"p99":1208.7991951895847}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:33+00","p50":572.57975375,"p95":714.4115521707703,"p99":1196.2558492440198}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:34+00","p50":597.315957,"p95":700.9393339392907,"p99":708.948962196289}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:35+00","p50":74.775034125,"p95":786.2939005012583,"p99":1170.7169751265478}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:36+00","p50":482.60468325,"p95":855.3105324376652,"p99":865.0547892986104}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:37+00","p50":103.325025875,"p95":762.8809218629713,"p99":773.5232681256733}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:38+00","p50":96.02715475000001,"p95":734.5673009472516,"p99":1325.9662273045915}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:39+00","p50":531.4270469999999,"p95":693.1873841978748,"p99":1215.9816522836252}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:40+00","p50":499.970047125,"p95":583.4869961723638,"p99":595.0839591101184}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:41+00","p50":158.65949012500002,"p95":662.6412723807756,"p99":698.8580821419686}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:42+00","p50":92.59214800000001,"p95":643.5263749795054,"p99":658.747385132381}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:43+00","p50":599.966383875,"p95":691.7387086076952,"p99":705.8730442091093}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:44+00","p50":91.271568,"p95":639.3564529367218,"p99":652.8660350363923}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:45+00","p50":72.64803950000001,"p95":765.0695069014962,"p99":783.7044065581459}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:46+00","p50":224.60680525,"p95":859.4215893035519,"p99":1458.6823311002786}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:47+00","p50":657.0967882499999,"p95":784.0065696761841,"p99":1326.7111374227234}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:48+00","p50":126.792883,"p95":742.5032051712255,"p99":758.5321417113228}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:49+00","p50":203.55210949999997,"p95":670.5946055706313,"p99":687.2382871072836}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:50+00","p50":97.7980935,"p95":720.6916417479803,"p99":1111.55580376483}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:51+00","p50":73.046909125,"p95":935.6999261262755,"p99":946.3479210781944}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:52+00","p50":694.96197925,"p95":865.0090883561028,"p99":910.1249647789216}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:53+00","p50":100.6198375,"p95":707.6333333419562,"p99":1289.333569365059}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:54+00","p50":517.027441875,"p95":686.728940572446,"p99":1072.158082783736}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:55+00","p50":121.959923,"p95":712.907758918158,"p99":1141.545262716629}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:56+00","p50":542.58187075,"p95":707.5460854211758,"p99":711.7418915201309}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:57+00","p50":556.424347,"p95":694.8624562617243,"p99":716.4530680973148}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:58+00","p50":98.583142,"p95":806.6416844855868,"p99":1379.1959477137366}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:26:59+00","p50":516.355269,"p95":702.0596162831066,"p99":718.154682180567}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:00+00","p50":393.28232449999996,"p95":552.9596369988246,"p99":581.2818252043962}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:01+00","p50":81.917729,"p95":455.8028311031718,"p99":649.6888028255777}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:02+00","p50":79.451989875,"p95":680.8986336028872,"p99":721.836487117187}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:03+00","p50":647.997942375,"p95":774.5076503159564,"p99":787.172442624251}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:04+00","p50":142.98182125,"p95":675.8350102074389,"p99":692.5839469815502}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:05+00","p50":212.964159,"p95":887.287762673423,"p99":1019.7128285008297}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:06+00","p50":604.6771767500001,"p95":729.5672299975791,"p99":1423.219974054393}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:07+00","p50":107.01938287499999,"p95":714.1546282191473,"p99":729.557328433682}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:08+00","p50":332.53217537500007,"p95":899.275784804231,"p99":994.8214940501854}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:09+00","p50":765.5129744999999,"p95":830.0676186091438,"p99":1543.1534070887587}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:10+00","p50":693.2378915,"p95":772.8298751328527,"p99":817.2021842414136}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:11+00","p50":666.11038425,"p95":789.4391706845779,"p99":1394.6037561038431}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:12+00","p50":83.13976149999999,"p95":860.6034167384224,"p99":868.2937076715928}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:13+00","p50":99.34673749999999,"p95":914.9234765746536,"p99":920.257051748474}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:14+00","p50":98.6697925,"p95":753.7742659125057,"p99":1294.8243443101464}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:15+00","p50":103.63059337499999,"p95":777.5305177989511,"p99":1405.0587452407408}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:16+00","p50":244.242021,"p95":816.3927154008838,"p99":1369.2963718732424}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:17+00","p50":588.43056275,"p95":768.6886736777767,"p99":786.9882227389684}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:18+00","p50":122.510505,"p95":757.9856339647728,"p99":1321.6215642758502}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:19+00","p50":106.51882175,"p95":758.9188347464166,"p99":1378.5895233026417}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:20+00","p50":76.5514765,"p95":895.9968648170876,"p99":935.7477452465744}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:21+00","p50":874.0132995,"p95":956.1482694704553,"p99":963.6102988693333}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:22+00","p50":583.308837125,"p95":932.7638205420501,"p99":1731.0836550471195}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:23+00","p50":94.040583,"p95":1030.2745532629913,"p99":1764.519123798602}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:24+00","p50":1030.3372645,"p95":1108.2828734860352,"p99":1260.693057416773}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:25+00","p50":103.86066712499999,"p95":1085.444426796652,"p99":1233.8153267238442}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:26+00","p50":919.275018375,"p95":1111.4812926524155,"p99":1125.6446993848122}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:27+00","p50":119.8476195,"p95":971.4126486125461,"p99":1106.4061524656274}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:28+00","p50":968.58334,"p95":1168.4518755390586,"p99":1358.1867475091137}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:29+00","p50":102.438989125,"p95":1217.009810465365,"p99":1353.0945071358221}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:30+00","p50":101.38889225,"p95":1131.5497298590406,"p99":2020.1454294459138}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:31+00","p50":80.804464,"p95":1060.257456138572,"p99":1254.4997144694667}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:32+00","p50":385.442498,"p95":1066.2100670115292,"p99":1904.6921858442797}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:33+00","p50":113.4405375,"p95":978.5809758332618,"p99":1877.3110686937546}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:34+00","p50":88.6283905,"p95":966.5635357893524,"p99":1065.520693228097}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:35+00","p50":576.2953837499999,"p95":957.5156771426272,"p99":972.1814312082972}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:36+00","p50":98.75214475,"p95":929.791263505456,"p99":1694.7972558777542}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:37+00","p50":807.8431465000001,"p95":914.3748681403212,"p99":1610.418089717793}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:38+00","p50":832.97939,"p95":928.9984297004289,"p99":940.7791056735764}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:39+00","p50":816.164922,"p95":947.4170790241334,"p99":1631.989257620045}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:40+00","p50":887.2680105,"p95":968.906848407711,"p99":1745.2840178505173}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:41+00","p50":111.901184,"p95":1031.8637241821662,"p99":1132.8850016964293}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:42+00","p50":926.723417,"p95":1024.3723080627965,"p99":1981.2809690914787}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:43+00","p50":775.38384525,"p95":1088.34300961757,"p99":1884.03352134523}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:44+00","p50":736.02827725,"p95":1023.0859533357171,"p99":1779.0555973208043}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:45+00","p50":289.20240924999996,"p95":990.0248177881402,"p99":1595.8536812171496}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:46+00","p50":798.483592,"p95":990.311879826519,"p99":1010.6289923758884}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:47+00","p50":97.180647,"p95":856.2998693171922,"p99":1517.098223408784}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:48+00","p50":98.7199995,"p95":1013.7102589735126,"p99":1599.9516589650154}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:49+00","p50":87.65754575,"p95":996.5109195071532,"p99":1027.103319774629}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:50+00","p50":643.250057,"p95":937.378297479924,"p99":1608.05313230258}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:51+00","p50":525.994260125,"p95":876.4638860210255,"p99":957.4505987909615}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:52+00","p50":562.914557875,"p95":942.050058719255,"p99":1829.2124822401279}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:53+00","p50":80.3542635,"p95":972.81844213558,"p99":996.4979222620588}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:54+00","p50":815.708826,"p95":1037.4644463261823,"p99":1837.950471542036}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:55+00","p50":112.67524925000001,"p95":909.4475599672691,"p99":1679.7008236232184}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:56+00","p50":1025.629469,"p95":1150.1334853116646,"p99":1161.8491805762558}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:57+00","p50":91.570122875,"p95":1132.0007746294718,"p99":1140.7634780749222}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:58+00","p50":107.27536649999999,"p95":1133.4587083249144,"p99":2185.395419288411}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:27:59+00","p50":104.704461875,"p95":1112.5196729516513,"p99":1147.1165315940632}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:00+00","p50":91.41501275,"p95":978.0664178243392,"p99":1001.4748644775543}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:01+00","p50":776.468757125,"p95":991.2807402107552,"p99":1813.7729236607804}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:02+00","p50":115.84937575000001,"p95":965.8773235770826,"p99":1036.069734800213}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:03+00","p50":662.89768525,"p95":981.2580391458989,"p99":1752.1709236342706}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:04+00","p50":765.6738244999999,"p95":924.4387809170039,"p99":1781.220989877018}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:05+00","p50":638.659328375,"p95":849.4510381980281,"p99":959.4825463357122}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:06+00","p50":101.71833649999999,"p95":739.7282660718714,"p99":1251.1206507716631}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:07+00","p50":82.9917605,"p95":830.1088627197873,"p99":896.6756156647004}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:08+00","p50":108.31075625,"p95":947.3498507881308,"p99":1668.7987246968537}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:09+00","p50":116.41726075,"p95":802.8718029322288,"p99":814.5471862992873}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:10+00","p50":93.96898775,"p95":710.2515431279696,"p99":732.5808000129521}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:11+00","p50":583.832229,"p95":647.0270034990291,"p99":698.2923848297272}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:12+00","p50":468.71242862500003,"p95":690.8711957290751,"p99":1235.6856886711641}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:13+00","p50":433.384498125,"p95":625.2445689571507,"p99":976.3922321855681}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:14+00","p50":413.237292,"p95":526.6098000994245,"p99":812.456552921618}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:15+00","p50":299.205116,"p95":492.99117771912574,"p99":605.4214663078155}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:16+00","p50":293.22894325000004,"p95":365.03064595188835,"p99":595.5498045542579}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:17+00","p50":273.58605125,"p95":424.6200828516103,"p99":456.98528143046286}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:18+00","p50":397.24886300000003,"p95":473.96061450162296,"p99":833.4292691989756}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:19+00","p50":363.8672185,"p95":448.33183248900457,"p99":799.4152067023049}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:20+00","p50":310.29933525,"p95":406.6611501658363,"p99":679.5418687178612}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:21+00","p50":307.8665185,"p95":488.69493318445734,"p99":782.4062110497742}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:22+00","p50":190.9970545,"p95":524.673747246353,"p99":836.1219399856725}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:23+00","p50":148.47164650000002,"p95":545.1169729325951,"p99":599.014142115266}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:24+00","p50":500.193814125,"p95":558.0838640106618,"p99":592.4603017157865}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:25+00","p50":132.601594,"p95":541.8790366612682,"p99":562.5801480311355}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:26+00","p50":183.482924125,"p95":492.4559795706622,"p99":510.1869819281184}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:27+00","p50":493.6613305,"p95":621.8051593475675,"p99":781.6253891031222}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:28+00","p50":108.16506512500001,"p95":780.896107925974,"p99":791.7862830251296}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:29+00","p50":481.04981537500004,"p95":535.022067790534,"p99":610.2130178524488}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:30+00","p50":150.33052825,"p95":589.9329382093747,"p99":1010.4399517955069}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:31+00","p50":514.467907625,"p95":566.9836782232621,"p99":585.351502965852}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:32+00","p50":399.30092575,"p95":572.0785591933184,"p99":1107.888050076954}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:33+00","p50":500.723357375,"p95":649.4083852290695,"p99":1094.2158242862301}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:34+00","p50":441.76848475,"p95":615.9269104549694,"p99":916.1288012483005}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:35+00","p50":411.405637,"p95":568.8404731797257,"p99":586.812882208088}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:36+00","p50":274.80392299999994,"p95":427.85475161022305,"p99":460.85597907953354}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:37+00","p50":217.81966225000002,"p95":507.2652116675975,"p99":824.3345104699449}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:38+00","p50":374.3692605,"p95":484.64795786478857,"p99":739.751715242342}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:39+00","p50":353.97543199999996,"p95":476.15321125156737,"p99":500.5607186569462}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:40+00","p50":321.38123087499997,"p95":436.620606630187,"p99":737.7168685059305}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:41+00","p50":334.344746875,"p95":472.6961797190179,"p99":486.23900481155016}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:42+00","p50":231.41239424999998,"p95":514.5408225845756,"p99":535.1737293680535}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:43+00","p50":368.113885,"p95":505.7880404197874,"p99":889.8334835519614}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:44+00","p50":340.63093000000003,"p95":544.8569763792494,"p99":828.8173506260566}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:45+00","p50":296.380214875,"p95":378.73815953984615,"p99":558.369150114219}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:46+00","p50":291.46888349999995,"p95":373.3985210453704,"p99":529.0144844072361}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:47+00","p50":177.727575875,"p95":557.414201620124,"p99":581.4608462152462}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:48+00","p50":574.6727935,"p95":662.2411552613785,"p99":1076.9168335844022}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:49+00","p50":470.683455,"p95":612.487010847492,"p99":627.2532934879735}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:50+00","p50":326.6330375,"p95":469.84280251758463,"p99":689.9574557334238}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:51+00","p50":315.200124875,"p95":371.2283929650301,"p99":384.78383619911335}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:52+00","p50":292.49602350000004,"p95":402.942831904795,"p99":585.9282898422131}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:53+00","p50":286.16027975000003,"p95":402.0746194851122,"p99":422.49362438208436}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:54+00","p50":322.4719315,"p95":516.1939021260692,"p99":846.7920776983127}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:55+00","p50":273.330161625,"p95":422.97001303794184,"p99":611.6772065243349}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:56+00","p50":352.57128150000005,"p95":481.8978179492218,"p99":653.3062959934584}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:57+00","p50":366.17549275,"p95":427.8414219488874,"p99":505.7875161142175}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:58+00","p50":371.2665585,"p95":449.1092208319499,"p99":694.6950897383529}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:28:59+00","p50":313.42803775,"p95":388.76410427984206,"p99":668.335757728529}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:00+00","p50":308.09589662499997,"p95":393.98089357468456,"p99":554.1746526815509}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:01+00","p50":301.49741425,"p95":388.7773890446139,"p99":558.3527908905065}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:02+00","p50":318.0489745,"p95":424.09963221047855,"p99":581.058825194397}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:03+00","p50":291.13363125,"p95":357.9098582223619,"p99":377.9731512978969}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:04+00","p50":299.973048,"p95":404.4110048307919,"p99":425.68230290712853}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:05+00","p50":300.542361,"p95":520.8627458552901,"p99":541.9274344406562}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:06+00","p50":265.6211285,"p95":397.8386968083267,"p99":582.743722058033}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:07+00","p50":257.6187775,"p95":409.23158811707054,"p99":443.3994520927825}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:08+00","p50":412.63374175,"p95":495.96256497760413,"p99":506.0841993760782}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:09+00","p50":307.32069,"p95":462.1593208798552,"p99":467.22281637964704}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:10+00","p50":284.46366425,"p95":474.53203099570203,"p99":804.1426800626917}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:11+00","p50":124.46035,"p95":466.93506133871864,"p99":479.8391090049753}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:12+00","p50":111.40056575,"p95":511.67594430733703,"p99":530.131415666482}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:13+00","p50":87.495375625,"p95":662.2147893807326,"p99":708.1286504168324}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:14+00","p50":94.58070099999999,"p95":721.4809653327447,"p99":738.0518806467078}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:15+00","p50":110.69369474999999,"p95":697.9406068355538,"p99":716.1012612715118}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:16+00","p50":214.182195,"p95":648.2101292256464,"p99":1122.9949680869724}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:17+00","p50":464.7840175,"p95":599.5309848749612,"p99":962.2964103759233}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:18+00","p50":108.33515650000001,"p95":547.9686579641282,"p99":565.8499861125746}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:19+00","p50":118.51101275,"p95":618.5439435254287,"p99":650.5347307286626}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:20+00","p50":108.88563475,"p95":707.738598052118,"p99":1140.0871652598776}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:21+00","p50":417.44715325,"p95":741.7296833160117,"p99":1273.3881753587943}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:22+00","p50":458.258155,"p95":544.5307816511566,"p99":553.8897006293536}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:23+00","p50":400.896122,"p95":534.4165344767547,"p99":778.3830348178458}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:24+00","p50":315.16322212499995,"p95":413.1671699261234,"p99":615.8459712129714}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:25+00","p50":297.118819625,"p95":344.8425547570999,"p99":563.2760653819863}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:26+00","p50":361.63912562499996,"p95":432.88471496795984,"p99":483.95452079534147}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:27+00","p50":301.98233074999996,"p95":361.222400976325,"p99":382.8317680066695}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:28+00","p50":337.503261375,"p95":442.99795398707533,"p99":724.1136077360472}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:29+00","p50":295.72484599999996,"p95":344.7310441643686,"p99":529.1874844701595}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:30+00","p50":345.627269,"p95":436.6147759825027,"p99":697.8943254438}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:31+00","p50":342.04246950000004,"p95":487.65300836813543,"p99":508.1545541713824}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:32+00","p50":323.19168287499997,"p95":399.84883587244644,"p99":594.0784729259535}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:33+00","p50":453.7813405,"p95":549.1498529480175,"p99":558.5076483486686}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:34+00","p50":307.51298225,"p95":514.2466757777727,"p99":666.7000180785838}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:35+00","p50":301.221184125,"p95":387.3535105626967,"p99":622.5290427808254}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:36+00","p50":232.71837962499998,"p95":454.5980763726817,"p99":702.6635981985826}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:37+00","p50":231.672261,"p95":497.5113689446612,"p99":511.7336395207216}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:38+00","p50":198.55294400000002,"p95":720.7092577161918,"p99":964.760130788135}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:39+00","p50":78.991288,"p95":725.5082415911851,"p99":1297.0628517812581}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:40+00","p50":147.928437875,"p95":680.5333862088868,"p99":1328.9682207190892}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:41+00","p50":513.589759,"p95":646.990380516503,"p99":673.1244882682299}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:42+00","p50":386.29462349999994,"p95":748.241572607583,"p99":1291.3040825689543}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:43+00","p50":115.33369175000001,"p95":704.8046756852979,"p99":833.9373177035592}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:44+00","p50":112.44316425,"p95":785.8506324674299,"p99":801.8507996728999}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:45+00","p50":103.30397174999999,"p95":612.0029275582558,"p99":1176.7423896429302}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:46+00","p50":96.417393375,"p95":755.6565918666439,"p99":1209.4588884046623}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:47+00","p50":116.7464295,"p95":752.0327589556434,"p99":761.511522652132}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:48+00","p50":117.1047545,"p95":747.771399122712,"p99":781.069464781327}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:49+00","p50":115.716701,"p95":759.8051980054311,"p99":1282.0206517551017}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:50+00","p50":585.7797497500001,"p95":703.5664625092487,"p99":776.2472850594062}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:51+00","p50":546.2585755,"p95":682.2849118457456,"p99":1236.3202590403937}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:52+00","p50":98.1363345,"p95":672.1944294032095,"p99":687.8424220977345}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:53+00","p50":100.52997775,"p95":707.7494626125886,"p99":794.967826357368}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:54+00","p50":636.3010205,"p95":806.3914718424263,"p99":1279.5051727237521}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:55+00","p50":416.19971375,"p95":629.0603040131788,"p99":1184.173905051052}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:56+00","p50":627.1579207499999,"p95":784.0821171477079,"p99":825.0210780889511}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:57+00","p50":481.581891,"p95":558.1941509429123,"p99":1253.6357631273706}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:58+00","p50":115.24757550000001,"p95":744.8417998455575,"p99":1421.2091283077777}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:29:59+00","p50":611.209067,"p95":691.4554627651119,"p99":703.8131257151996}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:00+00","p50":99.4396145,"p95":689.3038127549735,"p99":714.3330902485809}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:01+00","p50":408.9566325,"p95":686.5244953501301,"p99":703.909123255583}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:02+00","p50":237.39411525,"p95":676.6801362047275,"p99":694.615920708498}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:03+00","p50":313.599139,"p95":615.6114951308132,"p99":637.8929248538379}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:04+00","p50":86.0213065,"p95":599.5818691169263,"p99":1057.567954730877}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:05+00","p50":550.9209195,"p95":661.2132394198402,"p99":1159.341655339312}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:06+00","p50":534.578246,"p95":617.5065957974733,"p99":1057.9586937787205}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:07+00","p50":157.533234875,"p95":585.7407479988997,"p99":606.0954177368872}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:08+00","p50":350.864196,"p95":522.3418001544569,"p99":536.5448669375954}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:09+00","p50":409.449096,"p95":480.4718389388963,"p99":494.4888448895302}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:10+00","p50":339.53418575,"p95":432.4196925290951,"p99":741.9198728590682}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:11+00","p50":317.17556325,"p95":450.00201321986424,"p99":472.2302861739409}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:12+00","p50":286.142209,"p95":478.08638824298004,"p99":700.8283486624346}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:13+00","p50":291.444990625,"p95":368.57707015885137,"p99":472.679397169678}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:14+00","p50":341.935481875,"p95":395.08219537397156,"p99":617.8745485655318}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:15+00","p50":310.01908225,"p95":385.2380344633442,"p99":410.9023974440346}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:16+00","p50":288.261132375,"p95":396.5231095519901,"p99":633.1741268593927}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:17+00","p50":320.18666375,"p95":409.7893208433111,"p99":537.6846012208935}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:18+00","p50":359.00353262500005,"p95":516.168460591249,"p99":575.2854240065988}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:19+00","p50":323.31296299999997,"p95":440.2101647076206,"p99":528.2344754516297}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:20+00","p50":368.81042225,"p95":435.46158995100404,"p99":712.7180345834504}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:21+00","p50":362.88992225000004,"p95":473.3297307802031,"p99":552.8927890752459}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:22+00","p50":310.691186875,"p95":444.68249651812073,"p99":690.3366700814033}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:23+00","p50":300.73757975,"p95":417.78582856867314,"p99":692.5521526330718}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:24+00","p50":307.24236587499996,"p95":395.51555889398355,"p99":627.7706716074367}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:25+00","p50":312.03190475,"p95":381.1990796466198,"p99":572.8658115201177}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:26+00","p50":346.764580625,"p95":425.20245016810776,"p99":588.9971993106979}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:27+00","p50":298.0104309999999,"p95":389.3145647260546,"p99":499.55299320039677}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:28+00","p50":337.47934687500003,"p95":403.3003098475722,"p99":546.7348175440104}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:29+00","p50":287.8908615,"p95":408.8412187108097,"p99":684.8793805816498}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:30+00","p50":393.32930225,"p95":483.0373246432078,"p99":498.229085264286}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:31+00","p50":314.74352124999996,"p95":423.25736828039766,"p99":459.5171218252048}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:32+00","p50":299.773615875,"p95":369.0792132911724,"p99":525.3008704806549}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:33+00","p50":277.757943125,"p95":384.22736396121576,"p99":557.925844491383}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:34+00","p50":354.94176725,"p95":471.7073641550181,"p99":750.6010599407978}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:35+00","p50":351.87466825,"p95":423.29796805440196,"p99":473.09707903350164}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:36+00","p50":242.1849935,"p95":476.7343967627237,"p99":500.17072462916997}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:37+00","p50":148.42390025,"p95":499.25939628421116,"p99":848.5317007869568}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:38+00","p50":413.44035325,"p95":625.9654236895065,"p99":1067.1681654909135}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:39+00","p50":143.711543,"p95":578.6534944861587,"p99":598.8595997899732}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:40+00","p50":423.35653125,"p95":498.8110309380304,"p99":528.5749382035949}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:41+00","p50":372.56555149999997,"p95":461.3074801156167,"p99":478.1567964097807}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:42+00","p50":307.86806212500005,"p95":432.73943338390075,"p99":724.1571321904701}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:43+00","p50":300.631595,"p95":386.6112577863945,"p99":593.7832817712379}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:44+00","p50":291.4311705,"p95":382.5101335448981,"p99":498.9160361147411}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:45+00","p50":267.390463375,"p95":429.51092221147684,"p99":452.8647156557419}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:46+00","p50":230.568806,"p95":450.1182795901612,"p99":697.1608596333027}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:47+00","p50":217.0977995,"p95":491.258409337956,"p99":529.1963211607766}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:48+00","p50":92.745808,"p95":593.1473796643402,"p99":627.0769989838343}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:49+00","p50":497.091359,"p95":641.8143507327231,"p99":793.4081528259659}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:50+00","p50":453.0923325,"p95":526.3796919830809,"p99":942.8876806219769}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:51+00","p50":386.17530024999996,"p95":472.7235243975116,"p99":813.071196943377}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:52+00","p50":157.0964505,"p95":607.9136308673823,"p99":619.861640270839}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:53+00","p50":148.728584625,"p95":573.9277241809675,"p99":593.3054633570433}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:54+00","p50":419.69580425000004,"p95":604.807328445215,"p99":1063.9356078717271}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:55+00","p50":382.35328300000003,"p95":533.6277949141693,"p99":563.6078893328729}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:56+00","p50":414.451975,"p95":538.9413428070492,"p99":570.3054218189757}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:57+00","p50":355.690895,"p95":450.5379486127597,"p99":462.5561714222522}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:58+00","p50":325.47515050000004,"p95":373.7734871582165,"p99":560.9831776804814}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:30:59+00","p50":324.15056775,"p95":440.4907055278759,"p99":735.01988380616}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:00+00","p50":284.972096,"p95":432.6445310364905,"p99":658.5378995677522}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:01+00","p50":300.86294475,"p95":411.1924618883133,"p99":422.2515594531355}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:02+00","p50":314.134273625,"p95":465.3561305560432,"p99":769.6732317750099}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:03+00","p50":296.33245775,"p95":378.7473063704317,"p99":558.3426892548732}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:04+00","p50":293.74119025,"p95":363.1842295404118,"p99":539.4583960588938}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:05+00","p50":280.8381965,"p95":364.04924415375615,"p99":380.8280181193695}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:06+00","p50":191.112059,"p95":492.998707584043,"p99":503.6418436920805}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:07+00","p50":498.8620985,"p95":584.5749428024765,"p99":984.5512861994338}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:08+00","p50":346.188769625,"p95":543.9500739004299,"p99":589.7287673037574}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:09+00","p50":353.95703149999997,"p95":427.79643270794537,"p99":674.6441027084105}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:10+00","p50":375.75863212499996,"p95":475.77642738966995,"p99":682.725939793021}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:11+00","p50":251.19148174999998,"p95":457.36930622498346,"p99":777.2196813921495}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:12+00","p50":368.123419,"p95":464.5813241138575,"p99":532.8249060032568}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:13+00","p50":357.308287125,"p95":421.57801872077107,"p99":765.9451731628224}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:14+00","p50":372.743040125,"p95":427.57039351449635,"p99":700.0927221233885}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:15+00","p50":348.02925575,"p95":523.7880856133686,"p99":774.3112111536456}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:16+00","p50":281.427765125,"p95":355.70976043162943,"p99":404.27031891217587}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:17+00","p50":252.39238375000002,"p95":542.1357308372716,"p99":845.6051372889705}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:18+00","p50":562.1608827499999,"p95":695.5763373555196,"p99":788.3857961967857}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:19+00","p50":508.64761587500004,"p95":646.8806613154826,"p99":1091.2108214632608}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:20+00","p50":116.7876,"p95":620.7749129295663,"p99":1015.463793528718}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:21+00","p50":275.50085825,"p95":610.4386295234549,"p99":1006.2025305319133}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:22+00","p50":128.5291215,"p95":608.2735621373046,"p99":621.4140643238406}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:23+00","p50":538.7569625,"p95":659.0088774462497,"p99":1116.8929979896998}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:24+00","p50":363.881751875,"p95":578.0055956370622,"p99":923.234330342449}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:25+00","p50":412.764432625,"p95":495.7872958712646,"p99":795.612540860806}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:26+00","p50":267.506242625,"p95":489.47627315608526,"p99":517.9756212236435}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:27+00","p50":257.40246325,"p95":522.7970951983716,"p99":665.1569913980532}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:28+00","p50":348.52530087499997,"p95":473.42758304925377,"p99":783.7132860706403}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:29+00","p50":293.5528005,"p95":383.5979386720409,"p99":605.1221796751938}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:30+00","p50":247.854784,"p95":434.6468275910549,"p99":458.53939133834075}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:31+00","p50":298.279185,"p95":431.19593869595076,"p99":762.9597934482526}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:32+00","p50":234.443263,"p95":439.70150432656595,"p99":452.6007795976419}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:33+00","p50":287.39025575000005,"p95":537.5397002401945,"p99":866.8851775758162}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:34+00","p50":492.04947625,"p95":543.8328578085185,"p99":578.5926157974014}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:35+00","p50":176.5241595,"p95":527.243412044997,"p99":550.0906449223094}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:36+00","p50":109.72676712500001,"p95":543.9328229950424,"p99":926.2037822769064}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:37+00","p50":94.090372,"p95":565.9757397015254,"p99":581.1957243987927}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:38+00","p50":524.68493925,"p95":673.3092784752541,"p99":1093.1171857362478}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:39+00","p50":101.10851925,"p95":620.1937874707521,"p99":1082.9081655621967}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:40+00","p50":474.33923000000004,"p95":641.8305727319897,"p99":1096.4400732876725}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:41+00","p50":308.730740125,"p95":443.35688185824296,"p99":635.3201971919223}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:42+00","p50":327.86958225,"p95":428.10708946388627,"p99":447.5705240249749}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:43+00","p50":339.19509625,"p95":392.57141001289216,"p99":621.7052937281843}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:44+00","p50":261.594142875,"p95":439.568011641957,"p99":471.1477694083342}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:45+00","p50":309.2020185,"p95":470.2912395624479,"p99":487.34812693356895}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:46+00","p50":324.08799999999997,"p95":428.56039164857185,"p99":546.5758104715767}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:47+00","p50":320.4764055,"p95":375.10805958656834,"p99":550.6386569826241}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:48+00","p50":309.91875,"p95":375.53607672029153,"p99":632.8729747108399}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:49+00","p50":322.631393,"p95":458.77154685538267,"p99":556.0413241189112}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:50+00","p50":380.15227300000004,"p95":472.3672493025517,"p99":488.8845633268681}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:51+00","p50":439.15993100000003,"p95":529.6466047935497,"p99":876.3041608084264}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:52+00","p50":466.37928550000004,"p95":639.3855993243734,"p99":1043.5931063064543}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:53+00","p50":466.435268,"p95":563.5566265684338,"p99":599.4925906010036}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:54+00","p50":365.1649085,"p95":542.7085171879758,"p99":989.5995722793407}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:55+00","p50":229.829948,"p95":518.0783655667695,"p99":531.1068458742476}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:56+00","p50":485.1299945,"p95":584.2879189153061,"p99":595.3736975677796}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:57+00","p50":351.003233125,"p95":516.9672430061252,"p99":811.1553906002435}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:58+00","p50":312.95492687499996,"p95":433.20641980959044,"p99":493.35111950058223}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:31:59+00","p50":424.909586,"p95":695.9461849605858,"p99":721.6060328860002}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:00+00","p50":341.218476,"p95":501.248308690537,"p99":769.6599460020867}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:01+00","p50":271.96628675,"p95":423.94321877250576,"p99":444.46629411647984}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:02+00","p50":447.1814395,"p95":548.4958269752302,"p99":868.202500224062}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:03+00","p50":439.08672924999996,"p95":504.09883756719546,"p99":916.9280163507939}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:04+00","p50":192.23417362499998,"p95":536.045076441408,"p99":809.0770049133674}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:05+00","p50":353.261318875,"p95":424.1995446635095,"p99":768.9572308676967}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:06+00","p50":285.559592125,"p95":405.38971282303027,"p99":545.413741818263}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:07+00","p50":279.610102375,"p95":330.07938116499895,"p99":546.3249663646507}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:08+00","p50":323.113545375,"p95":370.6518377209684,"p99":392.2155489433839}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:09+00","p50":308.287196375,"p95":420.7371608829656,"p99":442.49519228327705}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:10+00","p50":268.885768625,"p95":427.91410875084307,"p99":707.8988451844618}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:11+00","p50":278.95898975,"p95":356.8870967258965,"p99":543.9113869131385}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:12+00","p50":279.2976465,"p95":346.2591991795148,"p99":515.1550496906433}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:13+00","p50":300.5932595,"p95":383.9941672280284,"p99":472.5551886259122}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:14+00","p50":304.21824150000003,"p95":368.7228344283709,"p99":624.560558057874}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:15+00","p50":274.850634,"p95":422.39582571562585,"p99":456.9100162536664}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:16+00","p50":305.2588415,"p95":407.0219411112177,"p99":456.8899694818542}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:17+00","p50":230.31299425,"p95":447.63820591727983,"p99":789.0163592158855}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:18+00","p50":391.47631287499996,"p95":451.22589348530903,"p99":777.6613826088683}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:19+00","p50":333.75279375,"p95":473.7949723928201,"p99":493.12092977438067}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:20+00","p50":301.68841275,"p95":384.33193134267873,"p99":483.8762011613903}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:21+00","p50":327.34360225,"p95":391.9992934067838,"p99":560.6862817757788}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:22+00","p50":309.653553,"p95":364.87455726961394,"p99":565.0202160146675}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:23+00","p50":315.04539850000003,"p95":359.3832956257391,"p99":569.322975953743}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:24+00","p50":332.219772,"p95":448.3582440940798,"p99":727.4918375084358}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:25+00","p50":287.17576325,"p95":358.4708690754944,"p99":402.59527822151136}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:26+00","p50":331.38683425,"p95":516.4434341774283,"p99":939.766110316905}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:27+00","p50":188.41315487499998,"p95":547.6269311636248,"p99":958.2213809461651}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:28+00","p50":97.8494765,"p95":622.1047108641599,"p99":963.0461168638009}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:29+00","p50":101.766673625,"p95":621.5625484132321,"p99":1114.8994193952049}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:30+00","p50":64.093311875,"p95":658.6204891174566,"p99":678.1971480247546}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:31+00","p50":70.174247875,"p95":740.9207568690908,"p99":1261.9802260902866}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:32+00","p50":214.83365924999998,"p95":692.5214177911824,"p99":884.6448484023333}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:33+00","p50":92.371005,"p95":708.6121918195262,"p99":1206.819682772169}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:34+00","p50":647.278312125,"p95":742.1707665246682,"p99":1291.788576493063}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:35+00","p50":610.54552825,"p95":725.0168029808118,"p99":1345.604979949176}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:36+00","p50":546.00628375,"p95":626.7000802977072,"p99":636.050742123325}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:37+00","p50":573.935341,"p95":652.125319447588,"p99":668.1880451115875}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:38+00","p50":352.31891525000003,"p95":674.6778229426651,"p99":1182.5039944148064}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:39+00","p50":596.030959125,"p95":678.7282764765732,"p99":686.2222141329785}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:40+00","p50":505.7138265,"p95":593.7313760928154,"p99":1092.5954715810024}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:41+00","p50":327.86994712500007,"p95":584.331081029433,"p99":716.2379010117717}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:42+00","p50":84.589383,"p95":798.6891005879099,"p99":1085.711403771594}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:43+00","p50":74.274058875,"p95":797.6250187690569,"p99":1480.4792320582358}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:44+00","p50":589.5169195,"p95":679.3757608528501,"p99":699.2876051478281}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:45+00","p50":592.98948425,"p95":660.1392532174305,"p99":684.7251196469917}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:46+00","p50":590.4641395000001,"p95":734.1294259898824,"p99":1268.6126201946183}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:47+00","p50":681.2831445,"p95":754.3915183308685,"p99":770.9076515750542}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:48+00","p50":115.176395375,"p95":741.3373048239835,"p99":1376.0097368165025}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:49+00","p50":97.289263,"p95":737.1999233186228,"p99":762.5539317653661}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:50+00","p50":89.2915735,"p95":630.3884435240183,"p99":1076.3833174022373}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:51+00","p50":95.58731549999999,"p95":684.8227012003497,"p99":693.2764013657844}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:52+00","p50":109.318205,"p95":689.8686834191094,"p99":706.9523683444748}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:53+00","p50":81.9219855,"p95":727.7415513649491,"p99":740.9494691096139}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:54+00","p50":355.9083395,"p95":704.5030086573398,"p99":717.9617568602042}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:55+00","p50":645.61113775,"p95":777.133358936886,"p99":1273.208997334176}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:56+00","p50":635.012168875,"p95":791.3455757260747,"p99":1313.3756276965692}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:57+00","p50":117.538149875,"p95":657.9666585556859,"p99":1186.9241644449382}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:58+00","p50":537.720474,"p95":646.8902877618387,"p99":656.2713860176844}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:32:59+00","p50":455.76607325,"p95":581.8388907474032,"p99":1048.8181051360334}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:00+00","p50":90.6428275,"p95":647.1983947418489,"p99":1028.635221563118}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:01+00","p50":581.710327125,"p95":661.05293345636,"p99":1175.146830363749}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:02+00","p50":400.677326,"p95":484.8886079978738,"p99":936.9639014718532}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:03+00","p50":409.9375145,"p95":504.4964254512752,"p99":852.7572577133252}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:04+00","p50":268.92019225,"p95":459.93179207563196,"p99":474.2345246169868}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:05+00","p50":293.40624375,"p95":359.66026375972336,"p99":530.0859384081602}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:06+00","p50":281.548929625,"p95":342.8396534958023,"p99":517.6105613086855}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:07+00","p50":279.77069125,"p95":367.27445016520574,"p99":590.7985479938039}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:08+00","p50":304.41229649999997,"p95":507.2166044584451,"p99":546.5096269496127}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:09+00","p50":314.604929125,"p95":420.4997722836318,"p99":488.3864071298759}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:10+00","p50":258.28672274999997,"p95":410.12773756027343,"p99":421.64069110334924}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:11+00","p50":391.222739875,"p95":441.5732412527193,"p99":452.2020857769115}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:12+00","p50":371.789452875,"p95":446.2979834490837,"p99":741.187714920674}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:13+00","p50":387.628153,"p95":437.1328380488015,"p99":524.8879410009054}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:14+00","p50":316.25109275,"p95":398.8171124571905,"p99":643.7098309306359}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:15+00","p50":293.31932099999995,"p95":473.1623502270588,"p99":823.4109836659245}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:16+00","p50":338.87682725,"p95":497.59371901025366,"p99":769.8289370879814}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:17+00","p50":295.22596475,"p95":346.4864550695753,"p99":370.4801157009687}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:18+00","p50":249.155561875,"p95":447.62511845580354,"p99":739.0404626445603}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:19+00","p50":103.274179,"p95":521.5187171021605,"p99":542.1656854418736}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:20+00","p50":172.52262975,"p95":600.063880933915,"p99":613.9934400611255}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:21+00","p50":536.8205177499999,"p95":615.9651485625519,"p99":622.8438594687073}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:22+00","p50":327.2219615,"p95":457.37098634321484,"p99":661.4567234353943}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:23+00","p50":292.958465,"p95":374.1218990739746,"p99":536.6035539687119}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:24+00","p50":369.63376925,"p95":427.25764393210693,"p99":437.4027839655547}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:25+00","p50":336.90208325000003,"p95":461.2508458785353,"p99":752.3133720991449}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:26+00","p50":386.8671215,"p95":458.2507764644003,"p99":590.7763877113953}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:27+00","p50":317.922933,"p95":384.50814310816355,"p99":580.0774980966477}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:28+00","p50":342.31499775000003,"p95":425.68218415928646,"p99":647.9962974737358}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:29+00","p50":303.39275899999996,"p95":344.0644006168533,"p99":364.87416570699406}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:30+00","p50":278.88407099999995,"p95":365.3970934776669,"p99":513.9716591437569}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:31+00","p50":273.88478749999996,"p95":366.93335379087375,"p99":514.3888204215507}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:32+00","p50":267.7505165,"p95":352.5985428073902,"p99":508.04622137005276}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:33+00","p50":318.95476025,"p95":381.73581032357157,"p99":409.0456437353835}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:34+00","p50":292.157762375,"p95":431.04655097048925,"p99":444.8652370486748}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:35+00","p50":293.058522,"p95":412.3645071065651,"p99":639.7604883857326}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:36+00","p50":277.243241375,"p95":371.77799184252933,"p99":648.7843422275001}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:37+00","p50":285.412737875,"p95":363.93614718648934,"p99":386.19002453214}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:38+00","p50":276.1953935,"p95":446.1952410322352,"p99":475.1577159727783}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:39+00","p50":270.775513,"p95":446.64945931616637,"p99":588.9850086069083}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:40+00","p50":301.5173215,"p95":409.32661395959997,"p99":631.6025982975979}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:41+00","p50":99.832976875,"p95":581.7746723671468,"p99":871.4073769828606}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:42+00","p50":467.15671725,"p95":716.345179259606,"p99":1244.683550361834}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:43+00","p50":88.2673675,"p95":655.1747669724647,"p99":1259.5056679167853}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:44+00","p50":296.6939245,"p95":680.9213211464987,"p99":699.8088886441135}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:45+00","p50":85.14407475,"p95":758.3070702451467,"p99":773.4831749918613}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:46+00","p50":653.625363,"p95":773.8567862654995,"p99":1256.0157342506714}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:47+00","p50":555.31535675,"p95":693.79171219957,"p99":1076.8040288562552}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:48+00","p50":498.31183787500004,"p95":623.3567139229832,"p99":1107.9173518756995}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:49+00","p50":480.661848125,"p95":620.4707378870431,"p99":1021.9098897746314}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:50+00","p50":480.650861,"p95":550.9681621529357,"p99":934.4203922681589}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:51+00","p50":121.795594625,"p95":633.8980787074473,"p99":985.180884141729}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:52+00","p50":128.514814,"p95":603.8192071410336,"p99":880.6297100421295}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:53+00","p50":154.687293625,"p95":555.2047553341704,"p99":987.377979543756}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:54+00","p50":129.702719,"p95":658.1766383764797,"p99":714.0191199601702}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:55+00","p50":485.9368605,"p95":670.4893606270731,"p99":777.1199763592195}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:56+00","p50":620.753536125,"p95":678.9890339860875,"p99":1224.7398744773702}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:57+00","p50":503.0591965,"p95":650.5341099129214,"p99":776.2254235382252}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:58+00","p50":385.58545,"p95":539.9319269536101,"p99":834.2407343956365}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:33:59+00","p50":372.48103525,"p95":505.12970786003683,"p99":749.778420353931}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:00+00","p50":285.85806,"p95":415.0195060627165,"p99":623.4224483333969}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:01+00","p50":153.07872675,"p95":541.4674392474459,"p99":957.5327497406731}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:02+00","p50":214.15269375000003,"p95":545.8068903641696,"p99":923.3104201774835}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:03+00","p50":333.3182195,"p95":511.91031529526737,"p99":881.2662680824774}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:04+00","p50":365.28222325,"p95":518.8872319302529,"p99":530.4804534142198}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:05+00","p50":394.992568,"p95":532.8614690941992,"p99":606.960487602024}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:06+00","p50":100.6036555,"p95":674.3966775297029,"p99":717.9828388612327}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:07+00","p50":88.16352975,"p95":672.2164020451341,"p99":683.6131735954866}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:08+00","p50":112.73216,"p95":792.0188203104364,"p99":1186.1571805227356}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:09+00","p50":671.661158125,"p95":829.9781721992414,"p99":1543.766986325151}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:10+00","p50":93.70628925,"p95":679.6783475308223,"p99":702.621831426405}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:11+00","p50":80.440258,"p95":680.4999074715471,"p99":1225.2136750847035}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:12+00","p50":93.095777,"p95":794.1330463776599,"p99":808.7121465982857}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:13+00","p50":104.83630325,"p95":805.8806001291906,"p99":1447.3174762869153}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:14+00","p50":649.409621,"p95":758.5319423527208,"p99":1451.398615959631}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:15+00","p50":419.624189,"p95":688.072705941,"p99":1042.2826485103665}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:16+00","p50":81.9480055,"p95":735.1965770107691,"p99":958.4637423295775}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:17+00","p50":589.597714,"p95":755.3841258782342,"p99":1233.0684720262523}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:18+00","p50":553.290617,"p95":629.326693224828,"p99":1015.3594980035334}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:19+00","p50":114.28630287499999,"p95":657.7973895184779,"p99":1069.4661628373726}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:20+00","p50":599.072849125,"p95":694.0114389643534,"p99":707.7331935676413}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:21+00","p50":582.075689125,"p95":719.0825088023464,"p99":1260.9618261110652}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:22+00","p50":541.2646905,"p95":627.5543018996806,"p99":1029.013496379385}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:23+00","p50":104.171722,"p95":709.3918644827048,"p99":725.9460336650109}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:24+00","p50":578.1912195,"p95":690.7599427709308,"p99":1129.6648479358066}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:25+00","p50":492.685752,"p95":620.3268570252761,"p99":1008.8212484799806}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:26+00","p50":123.53550775,"p95":723.9493759322742,"p99":1055.3894847295878}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:27+00","p50":91.7655285,"p95":806.1282635593096,"p99":814.102062489387}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:28+00","p50":613.538083875,"p95":720.8123597820808,"p99":764.3350786934724}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:29+00","p50":614.731326125,"p95":662.8988629896934,"p99":1227.8573947460611}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:30+00","p50":621.791732125,"p95":696.624562337284,"p99":715.506502846263}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:31+00","p50":549.6374065,"p95":624.5914124293506,"p99":1154.1044335088739}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:32+00","p50":102.47358575,"p95":637.9317734804757,"p99":1004.2966977194667}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:33+00","p50":124.05706425,"p95":576.0194514134764,"p99":595.165404777973}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:34+00","p50":85.25917125000001,"p95":638.3156505565801,"p99":681.2916291698632}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:35+00","p50":505.5634205,"p95":622.9810555722818,"p99":643.5788913108339}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:36+00","p50":74.779258,"p95":613.2006093046531,"p99":1119.309327049797}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:37+00","p50":86.55985225,"p95":669.6068960181802,"p99":1181.612570842412}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:38+00","p50":103.82813949999999,"p95":805.9373531379005,"p99":1334.8893823292083}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:39+00","p50":645.46981125,"p95":745.0933163142205,"p99":1494.1058234586183}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:40+00","p50":594.844703125,"p95":714.6950073574632,"p99":732.8394257879648}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:41+00","p50":533.223393,"p95":616.9144891492467,"p99":1086.4599514262086}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:42+00","p50":537.202241125,"p95":648.9611983072465,"p99":1131.2637664911135}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:43+00","p50":581.5583965000001,"p95":696.3357432487526,"p99":1102.5415605218657}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:44+00","p50":456.01599,"p95":654.3193860273628,"p99":1085.8888821850549}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:45+00","p50":94.33578274999999,"p95":608.9387809547756,"p99":678.7328887469084}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:46+00","p50":93.499977,"p95":768.4415355223632,"p99":1288.237658033434}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:47+00","p50":91.3198145,"p95":872.6269834112885,"p99":1388.029557031032}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:48+00","p50":60.41880999999999,"p95":952.5750071030996,"p99":1452.1601389454263}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:49+00","p50":780.791269,"p95":886.8754741003198,"p99":910.0782603089524}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:50+00","p50":732.544784125,"p95":885.0438019791882,"p99":910.4515908622744}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:51+00","p50":124.45312775000001,"p95":847.8973667434102,"p99":1541.0308088792249}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:52+00","p50":86.02941537500001,"p95":918.6306058208055,"p99":1661.373236892053}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:53+00","p50":84.1101575,"p95":840.9286976704127,"p99":1564.154960381617}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:54+00","p50":103.12295825,"p95":798.9470615769899,"p99":1489.231111561822}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:55+00","p50":252.58040025000003,"p95":715.2177123532362,"p99":743.6085912542067}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:56+00","p50":773.380493,"p95":907.2502293037016,"p99":1415.9130837363466}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:57+00","p50":833.5380432500001,"p95":907.2729756545085,"p99":953.550206768718}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:58+00","p50":584.046921875,"p95":867.6832430181828,"p99":885.416747136107}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:34:59+00","p50":110.107866375,"p95":644.8281157235822,"p99":1039.0755586773412}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:00+00","p50":425.74892524999996,"p95":588.0194260165285,"p99":1076.8721552290983}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:01+00","p50":469.278957625,"p95":555.2223004511384,"p99":857.1078972687651}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:02+00","p50":389.720458,"p95":481.08777798117427,"p99":911.4645610509767}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:03+00","p50":137.479021625,"p95":495.00552934485205,"p99":754.8787150612095}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:04+00","p50":185.5130685,"p95":485.6496128878479,"p99":814.0175676143494}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:05+00","p50":148.46889175,"p95":492.74331900107916,"p99":782.6715986912078}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:06+00","p50":211.34229775,"p95":556.667877805702,"p99":906.682232929157}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:07+00","p50":135.78933825000001,"p95":497.8026034853474,"p99":518.7672694177046}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:08+00","p50":80.92779675,"p95":717.3204275538994,"p99":848.4559575993817}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:09+00","p50":473.54834825,"p95":755.3217135708935,"p99":1164.658677633602}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:10+00","p50":428.83147212499995,"p95":540.1414926211224,"p99":561.5375545941494}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:11+00","p50":171.05179025,"p95":516.2519162790467,"p99":862.6306547947102}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:12+00","p50":368.002391,"p95":501.3071583312634,"p99":831.4194998344456}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:13+00","p50":421.672267625,"p95":479.70088786608983,"p99":731.1625182427556}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:14+00","p50":137.3184455,"p95":514.1205199520181,"p99":541.4740582200318}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:15+00","p50":113.87528499999999,"p95":555.8570210576077,"p99":938.1959659566193}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:16+00","p50":520.560610375,"p95":588.8565526755432,"p99":608.7591614364715}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:17+00","p50":309.94249325,"p95":665.5557970739085,"p99":680.3174686750441}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:18+00","p50":489.07466450000004,"p95":816.0051908967728,"p99":849.2571836078454}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:19+00","p50":109.30075550000001,"p95":801.006493049178,"p99":811.2911150046149}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:20+00","p50":593.08445325,"p95":668.1072686278236,"p99":1163.3207710010747}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:21+00","p50":602.3603968749999,"p95":758.4634409470628,"p99":803.6030121515521}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:22+00","p50":94.96911037499999,"p95":652.3172440892879,"p99":1126.1947414385854}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:23+00","p50":93.81362525,"p95":693.6515256926282,"p99":706.817723192976}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:24+00","p50":403.03490825,"p95":598.872352389799,"p99":608.2141114378135}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:25+00","p50":73.7120285,"p95":712.611350968335,"p99":1199.1345775266805}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:26+00","p50":433.03269200000005,"p95":837.2271301220856,"p99":987.8462765574035}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:27+00","p50":84.18498274999999,"p95":999.8657878589376,"p99":1068.1057927211075}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:28+00","p50":881.317523625,"p95":1018.3415230647795,"p99":1812.254243621041}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:29+00","p50":836.7570559999999,"p95":975.6994369810669,"p99":1692.2784937373963}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:30+00","p50":118.06447812500001,"p95":976.9003218343123,"p99":1667.861339467525}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:31+00","p50":818.00626325,"p95":1007.08768432541,"p99":1030.8325874646087}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:32+00","p50":736.5193750000001,"p95":837.998918407093,"p99":865.7901910608292}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:33+00","p50":104.492662875,"p95":852.6955822397285,"p99":1339.0396551288964}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:34+00","p50":82.878078,"p95":774.6876308719272,"p99":1398.3327769881573}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:35+00","p50":83.08541025,"p95":867.1658334828631,"p99":895.2458363252412}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:36+00","p50":117.006145875,"p95":978.7986302419001,"p99":995.7785490484009}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:37+00","p50":309.16563075,"p95":1010.3974976423905,"p99":1836.2747511174355}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:38+00","p50":109.03253412499998,"p95":1075.6340962760967,"p99":1093.2558574401453}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:39+00","p50":87.584498625,"p95":1083.6887013420094,"p99":1092.0244905032794}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:40+00","p50":109.69323,"p95":982.9172719682549,"p99":999.69085716744}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:41+00","p50":301.720027,"p95":1017.8740555070275,"p99":1727.95737873438}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:42+00","p50":852.49094975,"p95":959.1174073714991,"p99":985.7905365296776}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:43+00","p50":113.22998025000001,"p95":959.2767822208037,"p99":1764.7726697948647}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:44+00","p50":715.76963825,"p95":991.4469288480931,"p99":1582.7754760771677}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:45+00","p50":770.237277,"p95":854.3502514542197,"p99":886.6452408565402}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:46+00","p50":783.4536204999999,"p95":842.5104282751183,"p99":1431.859746505747}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:47+00","p50":100.54401387499999,"p95":839.8334109177238,"p99":1455.0296714544381}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:48+00","p50":601.51657675,"p95":915.3847469339058,"p99":933.6893552181034}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:49+00","p50":80.14744499999999,"p95":1002.2288818216665,"p99":1007.621388224112}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:50+00","p50":516.214235625,"p95":904.3748059453919,"p99":924.0963283188255}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:51+00","p50":895.061251,"p95":1109.7869358812352,"p99":1127.3738091890336}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:52+00","p50":954.7937655000001,"p95":1069.3778942224194,"p99":1936.230636279879}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:53+00","p50":812.136060625,"p95":951.2137085753868,"p99":989.6338701658086}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:54+00","p50":796.72692125,"p95":933.4683460378277,"p99":1655.7160359783738}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:55+00","p50":765.550232,"p95":884.8039846203346,"p99":1663.9675207789}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:56+00","p50":123.089010625,"p95":1038.1255942239027,"p99":1051.9821533167333}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:57+00","p50":91.15691475,"p95":1006.5420905096886,"p99":1815.1006231395531}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:58+00","p50":149.32192650000002,"p95":1105.93513739564,"p99":1892.320165269265}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:35:59+00","p50":124.47152774999999,"p95":924.0151146774858,"p99":1707.3750749372648}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:00+00","p50":119.073389125,"p95":812.7615417340074,"p99":834.1082496265254}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:01+00","p50":100.41013262499999,"p95":811.6575031736268,"p99":1356.0981946750726}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:02+00","p50":686.76607975,"p95":766.0417892158933,"p99":811.5442926148474}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:03+00","p50":88.10902225000001,"p95":735.8354146880937,"p99":1286.1219684049393}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:04+00","p50":683.684494375,"p95":790.6031870393261,"p99":812.4657235555568}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:05+00","p50":733.1298455,"p95":798.8080281974305,"p99":1438.4329913750114}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:06+00","p50":686.830869,"p95":811.8748373426275,"p99":1392.7967470743667}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:07+00","p50":753.765411,"p95":844.7656576829481,"p99":864.9420944473305}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:08+00","p50":769.937153125,"p95":856.7467869077171,"p99":1556.45566678769}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:09+00","p50":114.716816125,"p95":817.6332793521921,"p99":842.5365234462103}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:10+00","p50":78.643387875,"p95":719.9645445179519,"p99":1220.8679172824575}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:11+00","p50":285.04781725,"p95":689.6000394081273,"p99":1228.5700558815681}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:12+00","p50":95.473532,"p95":822.2943844980289,"p99":1282.744476708851}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:13+00","p50":701.44034175,"p95":807.1291732607514,"p99":1332.82156703975}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:14+00","p50":729.258205,"p95":833.3123681337228,"p99":1414.9574581493184}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:15+00","p50":599.518685625,"p95":729.8177993689048,"p99":1375.4517066567787}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:16+00","p50":115.77295,"p95":672.5670642847642,"p99":1084.2144322446122}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:17+00","p50":69.712082375,"p95":796.9501758688107,"p99":1286.3543163674622}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:18+00","p50":95.661797,"p95":1053.0654501987437,"p99":1144.7220213320604}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:19+00","p50":299.5163735,"p95":1057.7569388924085,"p99":1105.1402624982854}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:20+00","p50":97.104582625,"p95":949.015182915322,"p99":1150.5403258990557}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:21+00","p50":137.469618,"p95":1222.277430211106,"p99":1398.379264503647}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:22+00","p50":121.48637149999999,"p95":1165.063952773287,"p99":1186.6006514239587}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:23+00","p50":92.517022,"p95":1167.948520913663,"p99":2201.5925970739127}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:24+00","p50":69.6811685,"p95":1085.98908957415,"p99":1106.786275514347}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:25+00","p50":106.44554175,"p95":1035.1499999459854,"p99":1039.0260858723764}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:26+00","p50":122.3544995,"p95":1076.5244117625823,"p99":1087.76407015211}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:27+00","p50":103.24258575,"p95":912.8627325642767,"p99":936.1917463672656}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:28+00","p50":90.77378825,"p95":1088.4335190828292,"p99":1600.6207609751432}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:29+00","p50":519.04723125,"p95":1081.9400353980575,"p99":1898.9399288745308}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:30+00","p50":75.996598,"p95":965.9648775850335,"p99":1785.8988318676454}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:31+00","p50":84.04475250000002,"p95":999.4342779396579,"p99":1855.726490518319}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:32+00","p50":75.03978575,"p95":943.1682399762211,"p99":963.2420997685718}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:33+00","p50":851.976989,"p95":965.6469020393399,"p99":1747.3262652490735}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:34+00","p50":108.43585912500001,"p95":978.1584536851531,"p99":1734.4671597523225}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:35+00","p50":68.85118349999999,"p95":859.0258748963814,"p99":876.9010494384842}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:36+00","p50":92.49565924999999,"p95":823.943350811553,"p99":835.8668299601445}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:37+00","p50":828.0638517499999,"p95":908.0583236637175,"p99":914.4165482597989}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:38+00","p50":870.60775275,"p95":939.2528125447436,"p99":1731.142982267748}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:39+00","p50":676.2734943749999,"p95":905.012034169172,"p99":1496.4329968638601}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:40+00","p50":809.85354575,"p95":939.6815485174498,"p99":1548.4477852376524}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:41+00","p50":756.534406375,"p95":861.0950653753637,"p99":1554.1079028043587}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:42+00","p50":724.210976,"p95":937.6279912232743,"p99":985.9527072384017}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:43+00","p50":424.4932620000001,"p95":1017.1794216305125,"p99":1033.1216741453734}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:44+00","p50":653.8618265,"p95":801.9299577416497,"p99":814.6067581982155}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:45+00","p50":606.633514125,"p95":690.1493138753826,"p99":1278.0254925675054}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:46+00","p50":223.0599325,"p95":664.7992883191798,"p99":1073.1166227337656}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:47+00","p50":114.2552775,"p95":676.635674827934,"p99":1230.4032845877925}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:48+00","p50":112.08877,"p95":744.6160188537182,"p99":770.6102571356406}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:49+00","p50":604.5285105,"p95":731.8133245462951,"p99":745.9417655367389}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:50+00","p50":446.39158549999996,"p95":670.2294601353908,"p99":1168.1223293346166}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:51+00","p50":640.26583325,"p95":814.1915744238506,"p99":1299.1156613307037}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:52+00","p50":356.16399375,"p95":745.7296818926944,"p99":765.8748525832958}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:53+00","p50":533.46609,"p95":707.4321812034268,"p99":730.0296894735432}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:54+00","p50":120.203265875,"p95":726.2696589246455,"p99":1228.2483684403392}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:55+00","p50":349.434848125,"p95":515.2810711769717,"p99":570.7093737481454}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:56+00","p50":450.83635725,"p95":567.6459090177964,"p99":964.6393090630727}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:57+00","p50":133.39139300000002,"p95":559.741550153971,"p99":589.5098808948217}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:58+00","p50":191.00141037500003,"p95":574.9091063413122,"p99":622.253142944567}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:36:59+00","p50":420.04886,"p95":523.4713704615287,"p99":535.881549294569}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:00+00","p50":333.16840049999996,"p95":487.58952467686413,"p99":752.3103612544498}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:01+00","p50":229.67522075,"p95":405.07264542918267,"p99":669.828275705548}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:02+00","p50":266.89019625000003,"p95":412.457719891732,"p99":425.0896710553398}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:03+00","p50":296.1095385,"p95":393.02935656935335,"p99":467.03356608371257}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:04+00","p50":307.29437800000005,"p95":377.8766090124174,"p99":476.07819042484573}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:05+00","p50":293.39131975000004,"p95":343.7287549255946,"p99":539.3554220039316}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:06+00","p50":257.4222995,"p95":441.71339882331665,"p99":640.4638683692737}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:07+00","p50":297.63833125,"p95":480.5110303432872,"p99":753.3292161722865}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:08+00","p50":444.1191405,"p95":618.3745670035374,"p99":697.2179083566475}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:09+00","p50":536.03091225,"p95":639.057003396405,"p99":1064.1123035158848}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:10+00","p50":489.1017115,"p95":546.6194361414076,"p99":1015.1320999376433}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:11+00","p50":336.736410125,"p95":543.7768979373167,"p99":589.9489470735381}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:12+00","p50":416.214908,"p95":649.6771132909565,"p99":668.5275161154328}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:13+00","p50":363.62848525000004,"p95":507.9102781576324,"p99":896.9002720494747}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:14+00","p50":430.6971815,"p95":513.9368698727365,"p99":806.6398490759717}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:15+00","p50":124.53986900000001,"p95":571.6499288996356,"p99":961.7245692922196}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:16+00","p50":214.091651,"p95":586.7324421985917,"p99":1056.4646440698966}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:17+00","p50":456.58848725,"p95":552.1996754824701,"p99":854.1232292604961}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:18+00","p50":197.889004625,"p95":651.3298450198652,"p99":1135.6079021403136}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:19+00","p50":519.355633625,"p95":618.6375449165475,"p99":629.552142739419}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:20+00","p50":435.28712125000004,"p95":548.5659840999366,"p99":1102.4138083196613}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:21+00","p50":98.79266137500001,"p95":589.5028515196823,"p99":908.273179608808}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:22+00","p50":124.8897605,"p95":592.8022814374608,"p99":605.8667231950078}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:23+00","p50":379.2651835,"p95":557.8386224320817,"p99":950.3731878794537}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:24+00","p50":116.39718975,"p95":631.4402117343524,"p99":642.2463238989973}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:25+00","p50":90.15358950000001,"p95":608.8874860355959,"p99":649.516391126278}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:26+00","p50":624.763379,"p95":727.3070853458023,"p99":1342.5810174799385}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:27+00","p50":596.44017325,"p95":744.6131740976455,"p99":765.6806131331632}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:28+00","p50":499.5969417499999,"p95":611.1000208881114,"p99":975.7094854023018}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:29+00","p50":157.824604625,"p95":527.3114365028431,"p99":535.5996853598955}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:30+00","p50":436.69188325,"p95":596.2601924559565,"p99":606.5567729262219}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:31+00","p50":407.4674435,"p95":502.30323843585256,"p99":516.0079254376402}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:32+00","p50":258.22911162500003,"p95":427.2476045597876,"p99":484.89207355789495}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:33+00","p50":301.2179105,"p95":446.88229905512213,"p99":462.11944747333865}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:34+00","p50":287.996123875,"p95":404.03180142760897,"p99":579.5345565519021}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:35+00","p50":265.7287715,"p95":310.31915771487905,"p99":318.11029455968236}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:36+00","p50":310.195787,"p95":453.00818127738194,"p99":526.124747677278}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:37+00","p50":296.9225835,"p95":376.4406422336453,"p99":424.73409052188583}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:38+00","p50":348.02028375000003,"p95":402.41910148300553,"p99":640.3291383437652}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:39+00","p50":264.84465312500004,"p95":450.1315074929082,"p99":696.2639074159937}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:40+00","p50":239.04906825,"p95":561.8555744218346,"p99":838.3821117369895}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:41+00","p50":155.5696465,"p95":495.16517457399704,"p99":799.2089674692517}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:42+00","p50":88.92640600000001,"p95":610.1714482202842,"p99":625.8775527869924}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:43+00","p50":597.2469415,"p95":693.5683701426493,"p99":1218.9459327402458}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:44+00","p50":574.346524,"p95":675.0430863930416,"p99":1171.730417598198}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:45+00","p50":477.42914499999995,"p95":614.0004201643216,"p99":1181.2165996472434}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:46+00","p50":85.94973025,"p95":583.6862095821848,"p99":639.7566537083437}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:47+00","p50":530.7572759999999,"p95":636.5561101682209,"p99":999.0852185899308}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:48+00","p50":508.81486674999996,"p95":554.1236487526307,"p99":566.5012467010293}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:49+00","p50":86.6075395,"p95":638.0414933918576,"p99":655.1350934621928}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:50+00","p50":112.228305,"p95":811.2070735281964,"p99":1250.1425130779662}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:51+00","p50":108.16584262500001,"p95":951.9993394020863,"p99":1135.3904935063722}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:52+00","p50":96.25899050000001,"p95":962.7569585072803,"p99":979.4471106491155}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:53+00","p50":647.46179075,"p95":760.4775782476916,"p99":786.9958591603411}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:54+00","p50":719.2680055,"p95":772.5902221648762,"p99":1257.8308814236077}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:55+00","p50":98.2301315,"p95":818.989865038845,"p99":834.5822578009415}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:56+00","p50":104.61052799999999,"p95":924.4785488920014,"p99":1539.0725971217257}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:57+00","p50":99.694986,"p95":980.7589960955257,"p99":1059.418663187481}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:58+00","p50":61.504565,"p95":1058.479534721055,"p99":1070.9729452640654}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:37:59+00","p50":909.8282682500001,"p95":1111.5286913990617,"p99":1137.7327557222552}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:00+00","p50":774.7994755,"p95":913.7009828912716,"p99":929.7066031492158}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:01+00","p50":113.39086575,"p95":945.1006787542151,"p99":973.2315788999944}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:02+00","p50":75.765308,"p95":1034.5690115415318,"p99":1148.136567425405}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:03+00","p50":98.37229875,"p95":1070.263497585764,"p99":1083.2940468365875}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:04+00","p50":825.631722125,"p95":1015.3121484996934,"p99":1895.6215106100456}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:05+00","p50":795.6490488750001,"p95":931.2383762050907,"p99":1548.115543459381}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:06+00","p50":106.98505575,"p95":886.8210728389788,"p99":910.8867352966671}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:07+00","p50":346.13834575000004,"p95":897.8427136996752,"p99":926.9380714217007}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:08+00","p50":719.3905279999999,"p95":818.1018957417997,"p99":1509.3688091795334}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:09+00","p50":589.8583625,"p95":896.4906350885154,"p99":1454.3793692658196}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:10+00","p50":96.428682875,"p95":914.3972950373376,"p99":1024.4915351656123}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:11+00","p50":845.4633080000001,"p95":964.1496298741596,"p99":1679.6018336226882}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:12+00","p50":845.97982925,"p95":914.0285118857183,"p99":1740.9849238446084}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:13+00","p50":343.06173262500005,"p95":861.0046328584576,"p99":872.3001151745365}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:14+00","p50":103.562157,"p95":754.6275892785616,"p99":1391.4898483359}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:15+00","p50":98.37739225,"p95":730.6101184296763,"p99":735.1548493528096}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:16+00","p50":117.23409062500001,"p95":650.4877518651274,"p99":1243.3172646605437}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:17+00","p50":157.58524325000002,"p95":546.856928750029,"p99":801.1770879716663}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:18+00","p50":189.364838,"p95":524.5230621719842,"p99":548.6224658881373}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:19+00","p50":157.12457525000002,"p95":444.97566323527576,"p99":739.4132090021286}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:20+00","p50":71.572584,"p95":706.8946008597503,"p99":753.4558328294889}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:21+00","p50":296.028162375,"p95":788.8211424646518,"p99":796.1695137529099}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:22+00","p50":715.6191130000001,"p95":784.6624266687562,"p99":1349.4721065910046}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:23+00","p50":581.8185555,"p95":717.3994755576508,"p99":1368.463815471132}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:24+00","p50":90.09506562499999,"p95":795.4647780056774,"p99":813.400252586853}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:25+00","p50":85.30998274999999,"p95":840.1604866148009,"p99":859.930356535739}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:26+00","p50":87.2175145,"p95":834.9254802809655,"p99":842.5905545924793}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:27+00","p50":427.315022,"p95":883.3206178359422,"p99":900.0620497901954}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:28+00","p50":885.501038875,"p95":968.5336293926786,"p99":1055.1805333195903}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:29+00","p50":111.47056,"p95":931.8513968834857,"p99":1784.818685262184}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:30+00","p50":100.59642400000001,"p95":815.958152922308,"p99":825.5577634318423}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:31+00","p50":85.41380225,"p95":870.2707284041352,"p99":894.7628286761337}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:32+00","p50":293.242117125,"p95":954.8251752106991,"p99":974.259486347814}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:33+00","p50":90.970407,"p95":933.7060204523812,"p99":1754.5324022259408}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:34+00","p50":87.66404075,"p95":952.0501074883964,"p99":1733.9434730536298}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:35+00","p50":96.16706125,"p95":964.6923717730849,"p99":971.4606551532584}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:36+00","p50":859.1124053750001,"p95":962.7361855997865,"p99":1631.4679321383735}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:37+00","p50":92.5456465,"p95":854.3034984981971,"p99":1622.8462222233713}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:38+00","p50":86.37186075,"p95":937.7674021457757,"p99":958.984447011303}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:39+00","p50":83.5138905,"p95":1039.2662649277768,"p99":1792.112264883081}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:40+00","p50":706.915246875,"p95":906.291260608279,"p99":1684.0650222206896}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:41+00","p50":94.859669,"p95":1056.9187156301855,"p99":1178.6304712027347}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:42+00","p50":194.565136375,"p95":1062.0975811589153,"p99":1828.0665326248813}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:43+00","p50":803.7479814999999,"p95":892.0954650792164,"p99":1574.3407535200743}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:44+00","p50":274.4749405,"p95":820.5271948277292,"p99":1524.0519352419271}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:45+00","p50":103.140468,"p95":726.9737100637527,"p99":1231.412364069321}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:46+00","p50":674.1626545,"p95":788.67795360929,"p99":1364.1583468696729}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:47+00","p50":100.62343924999999,"p95":792.9665882364164,"p99":806.4285009684949}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:48+00","p50":183.911331375,"p95":884.5337383284062,"p99":929.8999890067439}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:49+00","p50":96.95234625,"p95":926.3185145810052,"p99":1004.8245712713633}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:50+00","p50":475.0400115,"p95":964.0756729032927,"p99":980.0439234489426}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:51+00","p50":873.0028715,"p95":956.0454048320189,"p99":976.1192521752187}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:52+00","p50":117.708192,"p95":1012.9558315351186,"p99":1028.0209676373977}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:53+00","p50":90.68505549999999,"p95":979.0529092530443,"p99":1905.8924018707276}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:54+00","p50":891.943924875,"p95":1050.871471855961,"p99":1068.0251726075655}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:55+00","p50":194.81121762499998,"p95":971.819848769752,"p99":1772.4146971276807}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:56+00","p50":909.05071325,"p95":1047.185949220997,"p99":1136.1705824207081}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:57+00","p50":118.056470625,"p95":1039.8717229529962,"p99":1060.4440735127864}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:58+00","p50":498.23565099999996,"p95":969.4914416024153,"p99":1706.2904584602547}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:38:59+00","p50":91.015484125,"p95":937.6686048174495,"p99":946.9071051816189}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:00+00","p50":766.741661,"p95":875.1199453969047,"p99":891.9649304839372}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:01+00","p50":739.711209375,"p95":831.1111418506486,"p99":1448.1394564244401}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:02+00","p50":891.7840245,"p95":1073.4206334018104,"p99":1243.408632201959}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:03+00","p50":876.70282825,"p95":1031.3188191803301,"p99":1900.3308076154287}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:04+00","p50":90.31098675,"p95":887.8584887596334,"p99":914.5549503737421}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:05+00","p50":70.2265915,"p95":868.5244427680028,"p99":1600.3916058140621}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:06+00","p50":88.698342,"p95":1002.5636273961773,"p99":1029.3473880164795}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:07+00","p50":879.2056522499998,"p95":959.8852028702563,"p99":1761.68337341431}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:08+00","p50":735.5626528749999,"p95":879.1614583968365,"p99":1531.786919928326}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:09+00","p50":612.2355298749999,"p95":751.5791992061553,"p99":1413.4017765602466}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:10+00","p50":107.26308775,"p95":618.4065204620559,"p99":632.7162096298247}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:11+00","p50":148.067554,"p95":637.8406513086633,"p99":1125.214932985683}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:12+00","p50":592.090147625,"p95":688.8163638391231,"p99":1129.7614548940317}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:13+00","p50":108.013838625,"p95":737.5009304946574,"p99":1311.7743488123833}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:14+00","p50":340.63108875,"p95":697.9378120128014,"p99":708.2764662157974}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:15+00","p50":100.6430695,"p95":612.8912638966241,"p99":634.298298819459}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:16+00","p50":99.726873375,"p95":598.8724897756529,"p99":609.9180974139429}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:17+00","p50":93.4274115,"p95":607.4653731966982,"p99":649.8777687757158}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:18+00","p50":137.624438,"p95":694.0534044021721,"p99":722.424651175972}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:19+00","p50":114.29132324999999,"p95":718.6409816126144,"p99":1204.8961714799982}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:20+00","p50":357.88062525,"p95":713.7781306247771,"p99":769.8503853940191}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:21+00","p50":100.98101249999999,"p95":867.0105595674265,"p99":1042.6879241047961}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:22+00","p50":630.1604355,"p95":797.5644514115422,"p99":895.5911420034146}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:23+00","p50":421.539132,"p95":861.0494169724328,"p99":873.3038489508876}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:24+00","p50":100.16612125,"p95":857.0163265884777,"p99":1551.2592778760818}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:25+00","p50":511.87771612499995,"p95":849.7085432783102,"p99":864.5577270525334}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:26+00","p50":213.67927362499998,"p95":986.7358917921451,"p99":1002.1672866059174}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:27+00","p50":759.71462225,"p95":903.7412950486205,"p99":1002.6163386363261}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:28+00","p50":791.79892825,"p95":883.3815779101479,"p99":965.8990550358734}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:29+00","p50":102.51319662499999,"p95":765.1274926323499,"p99":789.7671089572547}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:30+00","p50":102.292002875,"p95":727.9270795949203,"p99":1330.8424420401848}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:31+00","p50":82.42538862500001,"p95":778.312744730081,"p99":793.5091432058912}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:32+00","p50":98.61641125,"p95":662.7054008139027,"p99":1230.572462200275}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:33+00","p50":170.42984975,"p95":660.751532451303,"p99":1101.940951575658}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:34+00","p50":131.89861075,"p95":623.385846058291,"p99":1123.3011249375313}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:35+00","p50":440.893735,"p95":537.2536202508087,"p99":571.9499639173088}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:36+00","p50":103.38452687499999,"p95":476.5830659255797,"p99":812.1698296573454}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:37+00","p50":86.9353715,"p95":580.3718235997601,"p99":806.0470457422728}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:38+00","p50":85.63201725,"p95":788.9073078976683,"p99":1293.5431586486498}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:39+00","p50":754.1297579999999,"p95":899.2237946438378,"p99":1442.7197048949108}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:40+00","p50":646.976766,"p95":811.6869808073517,"p99":837.0536069950427}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:41+00","p50":119.955019,"p95":627.9310120501403,"p99":654.4205059910736}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:42+00","p50":522.371338,"p95":591.664594624985,"p99":604.0424490330566}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:43+00","p50":353.56266075,"p95":582.8201635560711,"p99":735.6738940295905}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:44+00","p50":303.86587000000003,"p95":466.6720457244473,"p99":803.897168360486}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:45+00","p50":332.535225,"p95":434.70555391658644,"p99":598.9597567420988}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:46+00","p50":303.7878425,"p95":370.04426253336527,"p99":649.5819918751449}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:47+00","p50":320.7344425,"p95":409.29454245851144,"p99":641.4916128627461}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:48+00","p50":301.26090800000003,"p95":421.78633705974653,"p99":590.3962275585765}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:49+00","p50":324.20119687500005,"p95":429.93939698055334,"p99":521.647272708966}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:50+00","p50":299.822143,"p95":380.637354537878,"p99":540.1260354298173}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:51+00","p50":312.664357,"p95":464.7293501063444,"p99":600.3385639156589}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:52+00","p50":296.803679125,"p95":444.1316959409142,"p99":728.2334892014503}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:53+00","p50":218.336725,"p95":418.30587669073293,"p99":445.0886679139862}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:54+00","p50":124.78636399999999,"p95":504.48475855329946,"p99":843.318885751133}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:55+00","p50":393.18121675,"p95":562.9345412830785,"p99":920.6599593022189}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:56+00","p50":545.894659625,"p95":653.0274530332468,"p99":1032.967441090142}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:57+00","p50":400.32770912500007,"p95":578.0601017344889,"p99":626.9062340469761}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:58+00","p50":185.40420849999998,"p95":564.0331768429937,"p99":577.1804390344358}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:39:59+00","p50":457.6308285,"p95":536.0502643392679,"p99":860.4320773841238}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:00+00","p50":278.220596375,"p95":511.94754144079354,"p99":860.8366070906086}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:01+00","p50":262.7768315,"p95":462.88850963378906,"p99":760.457779694376}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:02+00","p50":331.2056645,"p95":463.0433312499099,"p99":554.2772626992479}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:03+00","p50":252.50770225000002,"p95":283.0309733622056,"p99":369.6404258692725}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:04+00","p50":266.628799375,"p95":397.4095962137111,"p99":414.81650568505813}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:05+00","p50":330.67919725,"p95":415.2450325253649,"p99":690.3202489379559}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:06+00","p50":246.81784325,"p95":376.2209517758865,"p99":652.2991932482677}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:07+00","p50":416.52285175,"p95":479.53250439438915,"p99":496.8082390563646}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:08+00","p50":446.805840375,"p95":537.1470679792274,"p99":909.495299447876}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:09+00","p50":131.751305125,"p95":552.4550750762158,"p99":576.0982285698111}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:10+00","p50":77.858141625,"p95":541.4977419760321,"p99":594.0171680423457}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:11+00","p50":81.73376487499999,"p95":704.0306966956904,"p99":1091.9826540233466}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:12+00","p50":575.8115593750001,"p95":686.9869810424202,"p99":705.5557887181029}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:13+00","p50":570.89191825,"p95":679.5402045564892,"p99":695.3771180634517}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:14+00","p50":601.5455360000001,"p95":710.4477593006754,"p99":1117.2767081823158}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:15+00","p50":643.2277515,"p95":768.3641348017192,"p99":1395.5645986989193}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:16+00","p50":641.61211675,"p95":711.3068697007058,"p99":1251.7459091525225}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:17+00","p50":583.514287,"p95":723.8259939392641,"p99":1231.477710545954}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:18+00","p50":588.939655,"p95":719.8919333463423,"p99":1196.1525994339927}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:19+00","p50":615.9682467499999,"p95":683.3305578580766,"p99":803.1131823384003}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:20+00","p50":597.5958807499999,"p95":682.1686386413043,"p99":1092.2766479724928}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:21+00","p50":601.7912253750001,"p95":664.0363008717762,"p99":1153.9587138070156}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:22+00","p50":604.043737375,"p95":722.8478474059584,"p99":1185.578500227452}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:23+00","p50":389.51912975,"p95":792.9191323957469,"p99":1282.2084513416175}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:24+00","p50":556.7526567499999,"p95":727.0468446332658,"p99":1369.303520861151}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:25+00","p50":497.756050125,"p95":639.1968087135364,"p99":653.4665139137471}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:26+00","p50":94.422911,"p95":982.2375234240288,"p99":1005.0770412358322}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:27+00","p50":581.9642332500001,"p95":938.2543182060244,"p99":951.5946107951705}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:28+00","p50":99.68781625,"p95":784.106864613209,"p99":1483.9674991772633}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:29+00","p50":654.09089425,"p95":801.9799210614162,"p99":1305.498405269099}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:30+00","p50":373.14820125,"p95":773.4166417118206,"p99":829.142973108099}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:31+00","p50":813.422108375,"p95":999.9629793233263,"p99":1017.8242135790924}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:32+00","p50":393.0872455,"p95":949.4447922513523,"p99":956.4429549742161}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:33+00","p50":81.8305415,"p95":970.4239269199749,"p99":1030.594924210781}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:34+00","p50":846.0709038749999,"p95":973.3527666405784,"p99":1809.4880825263128}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:35+00","p50":360.697055875,"p95":846.1808196110604,"p99":859.6189638533581}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:36+00","p50":265.9340335,"p95":892.8934920629164,"p99":1009.5777155805829}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:37+00","p50":98.237826,"p95":991.89272476434,"p99":1589.9973366204624}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:38+00","p50":87.52072025,"p95":1004.950602223821,"p99":1015.2407334012108}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:39+00","p50":921.252309,"p95":974.438664397151,"p99":1827.3710585418967}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:40+00","p50":777.10566925,"p95":922.1192570881174,"p99":951.1845596719894}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:41+00","p50":112.51931450000001,"p95":825.9173611637855,"p99":1333.9251791428194}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:42+00","p50":121.1026285,"p95":807.8775639635451,"p99":906.7205126886106}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:43+00","p50":431.7393415,"p95":799.5457661584769,"p99":813.444261327239}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:44+00","p50":118.597644,"p95":807.4388454307572,"p99":1410.3450504507798}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:45+00","p50":85.40065275,"p95":842.4558999213068,"p99":916.0273809171902}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:46+00","p50":353.34075800000005,"p95":773.7914096047211,"p99":1522.7789984515962}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:47+00","p50":90.91082675,"p95":687.4250664210653,"p99":1094.4905604440917}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:48+00","p50":131.82911437500002,"p95":754.5817561806366,"p99":1271.231513238905}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:49+00","p50":94.97586150000001,"p95":768.9278589075287,"p99":1340.721164815834}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:50+00","p50":584.4237315,"p95":687.7724928092986,"p99":1291.5855674805746}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:51+00","p50":519.8973100000001,"p95":737.3629511831494,"p99":1183.973762930472}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:52+00","p50":160.91687075,"p95":699.3948473911223,"p99":1315.5463248172075}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:53+00","p50":297.93870512500007,"p95":681.0371500318955,"p99":782.2903298008688}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:54+00","p50":582.654496,"p95":746.4213775416183,"p99":1224.7762475251488}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:55+00","p50":126.735852375,"p95":521.0717799914066,"p99":957.618485868108}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:56+00","p50":479.58236650000003,"p95":638.4813388174206,"p99":800.5922654839821}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:57+00","p50":355.8118775,"p95":579.6938917699166,"p99":1129.9632748797635}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:58+00","p50":447.12783924999997,"p95":599.7003829907987,"p99":1003.8008559374141}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:40:59+00","p50":137.6091025,"p95":609.5535375108551,"p99":620.5910106653719}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:00+00","p50":150.716965375,"p95":667.3144468618235,"p99":935.4251813267393}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:01+00","p50":69.918706125,"p95":671.2822277727553,"p99":677.9779777174579}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:02+00","p50":581.585975,"p95":657.8273134419703,"p99":1192.6034425643768}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:03+00","p50":100.91780675,"p95":682.5228755864279,"p99":704.1396846464233}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:04+00","p50":77.983688,"p95":701.402189865087,"p99":1231.1717065906123}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:05+00","p50":302.5025675,"p95":696.3999078534332,"p99":709.8458185415518}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:06+00","p50":418.66173425,"p95":664.0834259620606,"p99":693.5131603008242}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:07+00","p50":103.887275125,"p95":644.1927185822789,"p99":667.2248434237632}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:08+00","p50":535.02539875,"p95":627.2307238551359,"p99":637.929177284605}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:09+00","p50":78.3522975,"p95":759.9498682173153,"p99":1220.3284468625927}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:10+00","p50":652.9045040000001,"p95":751.1860923382998,"p99":762.032447897812}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:11+00","p50":609.6565069999999,"p95":684.7622107534536,"p99":1139.4462037672815}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:12+00","p50":602.95905525,"p95":671.8565358167997,"p99":1215.9905641932794}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:13+00","p50":102.81253725,"p95":725.8524760010423,"p99":750.7059497697444}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:14+00","p50":272.072921125,"p95":652.3353603429059,"p99":659.2718658819103}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:15+00","p50":388.59656425000003,"p95":508.6885314375206,"p99":728.4926112085063}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:16+00","p50":105.85802050000001,"p95":507.3300466084652,"p99":755.9929815375748}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:17+00","p50":93.810082,"p95":642.1958011430436,"p99":786.5905200933971}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:18+00","p50":522.154574625,"p95":736.7307622916884,"p99":1207.4557206533893}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:19+00","p50":112.438018,"p95":597.1381827892372,"p99":1087.1173515194448}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:20+00","p50":132.17251325,"p95":557.6404612375583,"p99":575.5622074757557}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:21+00","p50":232.277096625,"p95":518.2638282276816,"p99":532.9781717298932}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:22+00","p50":348.78299937500003,"p95":415.9079116078748,"p99":650.7977329512913}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:23+00","p50":231.044159,"p95":538.3411522716514,"p99":851.2776134944789}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:24+00","p50":501.70598400000006,"p95":566.6740140810912,"p99":582.2404865060565}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:25+00","p50":125.35225775,"p95":628.400235651935,"p99":717.0317053670385}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:26+00","p50":692.799644375,"p95":744.7097940349205,"p99":1239.9133474204932}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:27+00","p50":523.1742615000001,"p95":689.0583371387528,"p99":808.2828163339958}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:28+00","p50":555.41872425,"p95":737.8293431639873,"p99":1201.8130125038251}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:29+00","p50":313.615802625,"p95":681.076651277319,"p99":1220.5990041108168}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:30+00","p50":111.966755875,"p95":850.1052810737675,"p99":869.8244905008714}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:31+00","p50":99.33324825,"p95":731.9456400229149,"p99":757.5522333393344}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:32+00","p50":625.55909625,"p95":772.7443744964495,"p99":1350.5476009331435}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:33+00","p50":608.27816325,"p95":692.8783796616949,"p99":1204.1606742981128}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:34+00","p50":355.31799525,"p95":696.2993348459523,"p99":1233.3148014511805}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:35+00","p50":615.853769875,"p95":678.0538080354713,"p99":1235.5434003808105}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:36+00","p50":547.0280897499999,"p95":632.1835367735738,"p99":1184.7243831112205}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:37+00","p50":567.07488425,"p95":692.4259903974755,"p99":737.0170055786348}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:38+00","p50":540.827019875,"p95":716.0926005235124,"p99":760.6648021848314}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:39+00","p50":91.676317,"p95":700.5171004518687,"p99":1114.4867567970105}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:40+00","p50":178.69757525,"p95":645.3175592156575,"p99":662.9090221604877}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:41+00","p50":510.80603299999996,"p95":576.293848717546,"p99":984.2512814769107}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:42+00","p50":489.17318550000005,"p95":571.9527253482502,"p99":604.0912721293492}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:43+00","p50":494.704189625,"p95":577.9087491615333,"p99":586.9192647035507}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:44+00","p50":292.01512075,"p95":419.8984678603382,"p99":430.15284955071735}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:45+00","p50":293.297228,"p95":368.41083398309326,"p99":376.7022071171341}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:46+00","p50":298.661863,"p95":396.2608051658204,"p99":622.9135991037544}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:47+00","p50":298.89410025,"p95":470.42868819018327,"p99":722.8563866910515}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:48+00","p50":375.432709875,"p95":469.1789351388002,"p99":721.2579622634144}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:49+00","p50":365.09405475,"p95":441.4929394641447,"p99":470.95483930819415}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:50+00","p50":177.2672235,"p95":469.82052428082727,"p99":820.7436241534119}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:51+00","p50":166.220073375,"p95":650.1860085608402,"p99":913.4945891448085}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:52+00","p50":164.55982749999998,"p95":675.1325165675573,"p99":691.027193572279}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:53+00","p50":440.20904987499995,"p95":490.126788798986,"p99":865.7886639512839}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:54+00","p50":255.30816400000003,"p95":496.71493385834543,"p99":513.1290399321217}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:55+00","p50":244.13032525,"p95":435.4520309567732,"p99":657.4864091463509}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:56+00","p50":398.05524975000003,"p95":553.8029121169707,"p99":651.1291229852791}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:57+00","p50":316.135141,"p95":386.6330403865498,"p99":637.2612387500749}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:58+00","p50":256.0703565,"p95":418.63937928885076,"p99":438.947776653595}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:41:59+00","p50":388.431577,"p95":497.84506599587223,"p99":731.3956758715195}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:00+00","p50":347.77592025,"p95":441.3432252885878,"p99":699.8771724896569}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:01+00","p50":342.26103875,"p95":443.8019705883477,"p99":744.2210664480763}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:02+00","p50":380.41621775000004,"p95":463.7957751111137,"p99":702.14285753752}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:03+00","p50":175.99056912499998,"p95":506.9441309818415,"p99":874.852517836548}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:04+00","p50":205.4010415,"p95":471.7635042466602,"p99":799.0829832229309}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:05+00","p50":395.246619,"p95":502.72728631184486,"p99":862.2341359588452}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:06+00","p50":241.3264175,"p95":439.8521429593563,"p99":676.655908346634}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:07+00","p50":269.45191,"p95":436.81857404425625,"p99":774.786837385849}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:08+00","p50":310.11954125,"p95":395.0993383767023,"p99":555.9335430415421}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:09+00","p50":274.87881125,"p95":422.98373554597475,"p99":629.2655784257786}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:10+00","p50":127.409833625,"p95":507.7835483042841,"p99":548.1219844428437}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:11+00","p50":85.82727075,"p95":581.8706046327933,"p99":610.3071810688365}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:12+00","p50":461.13062412499994,"p95":624.5497536411673,"p99":978.8654028228087}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:13+00","p50":388.05106450000005,"p95":622.2374611560898,"p99":1129.74284875591}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:14+00","p50":111.486629875,"p95":515.4114408790315,"p99":531.6882900417919}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:15+00","p50":104.08347574999999,"p95":572.3453708929638,"p99":877.9943777894297}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:16+00","p50":60.781409625,"p95":638.9269557112557,"p99":670.1866636060261}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:17+00","p50":571.5319525,"p95":675.0714452929692,"p99":690.4449631936183}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:18+00","p50":682.9671907500001,"p95":788.2577114278994,"p99":1290.3770408297323}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:19+00","p50":155.8511985,"p95":787.2225689075856,"p99":806.8853236336003}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:20+00","p50":123.55665624999999,"p95":709.184460246296,"p99":754.9362648896847}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:21+00","p50":98.5199205,"p95":644.086141023056,"p99":1184.616580862573}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:22+00","p50":99.181009625,"p95":757.859096259973,"p99":789.3697877738709}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:23+00","p50":116.45557549999998,"p95":706.6016241157314,"p99":764.4780002739031}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:24+00","p50":529.8403412499999,"p95":620.6571123692876,"p99":1054.013055299844}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:25+00","p50":390.92585875,"p95":529.3831054400497,"p99":553.5651179911317}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:26+00","p50":362.165527,"p95":488.64172981225704,"p99":498.3419788258114}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:27+00","p50":337.20216512499996,"p95":412.4387678662996,"p99":509.96824086691163}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:28+00","p50":343.17089799999997,"p95":453.5341386786496,"p99":564.4847030900464}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:29+00","p50":296.50199299999997,"p95":391.2641547125664,"p99":490.94188533702084}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:30+00","p50":297.5637355,"p95":407.6023808442907,"p99":422.0883219411431}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:31+00","p50":301.27057249999996,"p95":378.7905963146374,"p99":534.073382950057}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:32+00","p50":318.3217705,"p95":383.47362892896655,"p99":444.5253650622253}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:33+00","p50":309.6437695,"p95":402.8811814281898,"p99":538.6379656667585}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:34+00","p50":266.117927,"p95":347.88802627715546,"p99":557.0639612257869}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:35+00","p50":271.453795,"p95":377.5706411838505,"p99":614.1780432585268}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:36+00","p50":299.894283,"p95":373.2629035923363,"p99":532.6508732361398}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:37+00","p50":263.32381225,"p95":383.4703171126944,"p99":403.1626735249851}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:38+00","p50":321.130054375,"p95":467.56678536297204,"p99":679.2235100588572}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:39+00","p50":304.5645025,"p95":370.3980582027921,"p99":601.9693522372346}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:40+00","p50":295.7079055,"p95":355.95701386484575,"p99":593.5692782461929}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:41+00","p50":289.558127125,"p95":368.496281160478,"p99":527.731354787498}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:42+00","p50":352.92710024999997,"p95":415.8891404933734,"p99":667.077242781611}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:43+00","p50":370.597242125,"p95":502.8027229448339,"p99":800.2706876158996}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:44+00","p50":287.00973650000003,"p95":339.84868146195333,"p99":563.3398707625796}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:45+00","p50":297.116771375,"p95":359.76352794321366,"p99":377.72960397386885}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:46+00","p50":254.85645900000003,"p95":398.866385972877,"p99":419.58558486942724}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:47+00","p50":356.24695499999996,"p95":450.67937455393934,"p99":724.1146656407949}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:48+00","p50":199.50586175,"p95":458.3767198018282,"p99":725.752709201582}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:49+00","p50":383.81741224999996,"p95":467.6560843397356,"p99":765.9955337157719}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:50+00","p50":351.64657775,"p95":563.7887574554212,"p99":876.5142034097424}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:51+00","p50":349.031694625,"p95":477.3299506356142,"p99":529.1141771366209}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:52+00","p50":321.6119955,"p95":376.87722231032706,"p99":623.1467286530761}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:53+00","p50":239.42122387499998,"p95":340.3126502574798,"p99":415.4955415524068}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:54+00","p50":244.83353150000002,"p95":449.76804482147713,"p99":463.36754414280944}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:55+00","p50":383.7817845,"p95":511.4800581631952,"p99":800.0995449182701}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:56+00","p50":328.7677545,"p95":539.6731094571339,"p99":851.3100248022298}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:57+00","p50":328.93003,"p95":406.0490670255678,"p99":421.5149166913147}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:58+00","p50":311.37318675,"p95":382.6825023511815,"p99":459.39835274513894}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:42:59+00","p50":278.96206662500003,"p95":338.5804798094388,"p99":358.3400600827739}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:00+00","p50":287.8355235,"p95":444.8997620010948,"p99":483.9082799245891}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:01+00","p50":307.6754525,"p95":391.47657433429265,"p99":601.8216982304097}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:02+00","p50":304.56908475,"p95":406.9372452528129,"p99":688.1658540305481}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:03+00","p50":379.37526912500005,"p95":475.9559734205691,"p99":763.8655060774242}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:04+00","p50":280.25102937500003,"p95":410.39659494381317,"p99":598.493936585872}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:05+00","p50":262.06911375000004,"p95":412.5971065830641,"p99":431.41030073017123}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:06+00","p50":272.37167875,"p95":379.6151189937395,"p99":451.8509663751662}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:07+00","p50":305.002048625,"p95":468.584363009354,"p99":723.6206617293608}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:08+00","p50":278.41478675,"p95":423.2138657986084,"p99":613.9337182775063}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:09+00","p50":330.87488325,"p95":426.69763431700665,"p99":450.90454449319907}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:10+00","p50":395.07971087500005,"p95":454.06801371536784,"p99":680.6671699121463}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:11+00","p50":319.3300605,"p95":429.7223667934785,"p99":453.630875939682}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:12+00","p50":201.040784,"p95":490.77628226164103,"p99":671.3031091490965}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:13+00","p50":241.82969887500002,"p95":445.57879789670136,"p99":469.50769435391135}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:14+00","p50":251.86527412499998,"p95":438.78317585765427,"p99":740.0937850824735}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:15+00","p50":283.45873274999997,"p95":478.2123564541151,"p99":804.6784587054359}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:16+00","p50":323.340585375,"p95":471.9255826540456,"p99":479.7646531583414}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:17+00","p50":342.398227375,"p95":416.11605904703407,"p99":734.3029239066334}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:18+00","p50":305.3725895,"p95":351.7808998242936,"p99":583.0842672720738}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:19+00","p50":277.97955950000005,"p95":350.2133625182836,"p99":511.6241912441497}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:20+00","p50":289.583161,"p95":327.8447180824288,"p99":501.7578077556095}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:21+00","p50":274.91456725,"p95":309.3723698600716,"p99":448.5433449242549}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:22+00","p50":241.977298,"p95":435.8780070107219,"p99":477.5913031914587}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:23+00","p50":384.1921275,"p95":454.94914608617387,"p99":481.54001450220204}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:24+00","p50":175.77593374999998,"p95":539.3298421974265,"p99":547.5865485812784}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:25+00","p50":131.1036575,"p95":544.2529143918418,"p99":974.2048014487609}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:26+00","p50":102.97879775,"p95":652.1670073142374,"p99":657.8782247398758}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:27+00","p50":468.85457675,"p95":756.2949275462917,"p99":779.773662232749}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:28+00","p50":110.86218725,"p95":660.263843659605,"p99":679.0366783134656}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:29+00","p50":89.53492650000001,"p95":676.053924952261,"p99":1062.4943778939657}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:30+00","p50":71.08362062500001,"p95":710.9521058317256,"p99":750.5769988000649}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:31+00","p50":596.47449775,"p95":695.9604923756895,"p99":729.0283574419517}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:32+00","p50":569.0933829999999,"p95":626.992993639925,"p99":1144.4499567489156}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:33+00","p50":539.2470854999999,"p95":666.699622981347,"p99":1135.6306223822085}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:34+00","p50":94.222186125,"p95":584.8607141380785,"p99":615.3729447414761}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:35+00","p50":549.25867075,"p95":706.2389536829588,"p99":713.1354258112622}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:36+00","p50":415.197247,"p95":579.2111818004474,"p99":1074.117541778147}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:37+00","p50":134.682393,"p95":516.0487999545304,"p99":531.191895994452}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:38+00","p50":393.60643749999997,"p95":529.091380848401,"p99":670.2554537576738}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:39+00","p50":294.485368,"p95":408.52738064484925,"p99":666.8754022226371}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:40+00","p50":390.962861,"p95":464.8704719083461,"p99":485.2160320277093}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:41+00","p50":279.455061375,"p95":450.1925215458091,"p99":754.4831757712993}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:42+00","p50":294.385962625,"p95":359.3989783361436,"p99":527.7422380271632}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:43+00","p50":342.096562,"p95":430.66033481475836,"p99":477.2222770835924}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:44+00","p50":386.0822445,"p95":456.97222139411735,"p99":733.9573791362305}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:45+00","p50":283.165253625,"p95":427.4109288410286,"p99":730.5271198289626}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:46+00","p50":252.78025200000002,"p95":459.72910177560254,"p99":685.1580301903587}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:47+00","p50":152.11253875,"p95":622.666891798,"p99":886.5028107622938}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:48+00","p50":355.48016974999996,"p95":601.30107048595,"p99":609.8211970710294}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:49+00","p50":338.713926125,"p95":477.36653575393836,"p99":648.6070639583114}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:50+00","p50":288.7456885,"p95":451.56101464781005,"p99":620.2607141391248}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:51+00","p50":451.82996,"p95":513.234633271504,"p99":858.8174380613765}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:52+00","p50":384.54756275,"p95":497.7919510117699,"p99":760.2437622812977}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:53+00","p50":297.965639875,"p95":385.5686904775392,"p99":508.82212643845105}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:54+00","p50":295.44791275,"p95":364.85309211976335,"p99":375.5118835829046}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:55+00","p50":253.043546,"p95":359.99909187732504,"p99":479.1938286464751}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:56+00","p50":427.6231755,"p95":501.52072665873897,"p99":564.5526673110209}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:57+00","p50":363.6350385,"p95":432.65036151670694,"p99":715.8104716866718}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:58+00","p50":335.055197,"p95":418.9552234252081,"p99":450.2736695102196}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:43:59+00","p50":309.46469825,"p95":411.3003049063931,"p99":420.2997286685047}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:00+00","p50":311.740323,"p95":408.00745924915765,"p99":606.0708448680525}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:01+00","p50":249.792659875,"p95":473.0294865705852,"p99":755.8735752447438}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:02+00","p50":127.249674,"p95":503.57950629789923,"p99":788.9254329133757}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:03+00","p50":320.03162925,"p95":488.99416620192153,"p99":721.1503014500835}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:04+00","p50":166.171437125,"p95":484.9673020281106,"p99":688.6786283997974}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:05+00","p50":472.877368,"p95":526.184564590721,"p99":538.2094878456803}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:06+00","p50":458.67030575,"p95":570.9150244892245,"p99":963.2870983263111}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:07+00","p50":512.5947693749999,"p95":578.7827999924057,"p99":598.4211829048128}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:08+00","p50":384.862007875,"p95":588.9129665492391,"p99":990.7341323646576}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:09+00","p50":213.36417975,"p95":479.8445885258408,"p99":809.6483500132293}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:10+00","p50":368.3238615,"p95":480.4240672756453,"p99":514.4323451134894}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:11+00","p50":308.368785,"p95":431.8099474417428,"p99":586.9957929687171}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:12+00","p50":202.0106995,"p95":468.1334880496415,"p99":594.9735002008546}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:13+00","p50":120.64307725,"p95":546.9710705114995,"p99":577.4064936388088}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:14+00","p50":415.849488,"p95":609.3847872141547,"p99":1045.777418429718}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:15+00","p50":79.28362225,"p95":587.4755849209697,"p99":596.0525755749414}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:16+00","p50":414.79693025000006,"p95":605.7086555829878,"p99":1110.0998665246316}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:17+00","p50":251.254127875,"p95":613.4338265122029,"p99":624.2905557171443}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:18+00","p50":154.81405275,"p95":582.3037358713933,"p99":602.6175252378497}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:19+00","p50":452.2145755,"p95":536.1660319557657,"p99":562.2590749970299}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:20+00","p50":445.9164485,"p95":539.6338126709213,"p99":557.9276089985503}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:21+00","p50":479.78094150000004,"p95":572.3563248870067,"p99":1041.5545725552977}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:22+00","p50":457.63024887499995,"p95":590.5546257798678,"p99":920.1034966194308}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:23+00","p50":93.650775,"p95":570.6612724479652,"p99":1044.6888917099952}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:24+00","p50":90.32240412499999,"p95":689.4508304285174,"p99":755.2752831247577}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:25+00","p50":110.7871615,"p95":719.0525781741859,"p99":1295.9060783479902}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:26+00","p50":109.34086725,"p95":774.4895784576137,"p99":898.6889978070946}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:27+00","p50":570.5188425,"p95":749.884418329052,"p99":1398.813382955803}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:28+00","p50":80.925664625,"p95":833.3813539482247,"p99":854.3655634814022}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:29+00","p50":117.998254,"p95":853.3049463555749,"p99":884.3721462162371}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:30+00","p50":755.06357275,"p95":812.1532248170195,"p99":882.2311292833976}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:31+00","p50":708.7799982500001,"p95":775.2739175596756,"p99":788.6307644151582}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:32+00","p50":407.361452375,"p95":667.0130622653558,"p99":1227.2931186189574}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:33+00","p50":153.4186675,"p95":429.21364306506297,"p99":449.3347473282566}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:34+00","p50":257.78902975,"p95":458.9191192238192,"p99":472.5831349939778}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:35+00","p50":345.14340274999995,"p95":450.9769894995396,"p99":477.3296527806902}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:36+00","p50":341.27113825000004,"p95":385.96132250056064,"p99":642.1374391282523}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:37+00","p50":237.3797785,"p95":324.73076448346615,"p99":443.89841352878193}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:38+00","p50":299.576333,"p95":495.5821607856075,"p99":505.39719759775164}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:39+00","p50":398.7052375,"p95":458.1014224267575,"p99":789.0102929351744}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:40+00","p50":417.977079,"p95":469.02213700240196,"p99":487.43367965049356}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:41+00","p50":342.335428,"p95":523.3921844312343,"p99":555.2638574658279}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:42+00","p50":389.135501125,"p95":523.7283988997092,"p99":895.5158643152869}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:43+00","p50":211.70509675,"p95":496.5825572793164,"p99":854.692638769352}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:44+00","p50":109.83256,"p95":535.7601701166996,"p99":560.3471649236827}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:45+00","p50":66.818347,"p95":627.5193733604384,"p99":702.6567142178047}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:46+00","p50":78.040681,"p95":707.4531985744328,"p99":1181.2490816531088}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:47+00","p50":83.5551835,"p95":759.727431839356,"p99":1318.324772638009}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:48+00","p50":77.63969399999999,"p95":788.3971868803263,"p99":825.680070505865}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:49+00","p50":87.95669874999999,"p95":850.1397595349702,"p99":867.8647324295254}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:50+00","p50":87.79954375,"p95":887.0016678767229,"p99":917.3669024762455}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:51+00","p50":259.97531975000004,"p95":880.528695598738,"p99":1620.1589157328683}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:52+00","p50":674.53009075,"p95":837.0256042757402,"p99":1416.1600269266721}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:53+00","p50":624.2255185,"p95":709.9139133832206,"p99":1139.8617646338005}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:54+00","p50":92.182633,"p95":740.0249289948185,"p99":749.0386667635127}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:55+00","p50":97.90971637499999,"p95":834.7431527553662,"p99":1376.4137333843487}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:56+00","p50":590.1255424999999,"p95":880.3871285600267,"p99":889.2875628825378}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:57+00","p50":545.3815875,"p95":808.4425583118882,"p99":1375.5368974390722}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:58+00","p50":300.0513725,"p95":558.7101303386297,"p99":963.6848099558925}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:44:59+00","p50":83.043756,"p95":651.3253709768672,"p99":675.6000771566434}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:00+00","p50":101.56621200000001,"p95":615.3566409862594,"p99":666.3373397240409}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:01+00","p50":577.765093,"p95":663.7702963915393,"p99":1119.9010246671587}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:02+00","p50":108.854346625,"p95":649.0579683519946,"p99":788.2339126711579}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:03+00","p50":534.3885009999999,"p95":655.611540535234,"p99":1058.3823055870446}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:04+00","p50":671.143941,"p95":769.3251629294261,"p99":783.9608344640632}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:05+00","p50":315.87011725,"p95":746.9830206000385,"p99":764.1539593560325}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:06+00","p50":94.244821,"p95":707.4628789442221,"p99":1336.6107392150054}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:07+00","p50":81.24592100000001,"p95":759.905684046274,"p99":780.0893932634067}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:08+00","p50":68.89709887500001,"p95":901.9222925808022,"p99":1000.6747901144843}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:09+00","p50":101.966354375,"p95":965.6572239342452,"p99":980.554538633899}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:10+00","p50":93.4332525,"p95":836.4901951003775,"p99":860.809486535294}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:11+00","p50":77.784846,"p95":877.2262407908094,"p99":1523.7769984597112}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:12+00","p50":65.354991625,"p95":962.5923084784687,"p99":1124.9792615734648}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:13+00","p50":831.2059194999999,"p95":1010.8299114349517,"p99":1780.7907197455197}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:14+00","p50":797.5591254999999,"p95":878.3298482781759,"p99":1699.3717159473392}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:15+00","p50":557.203861,"p95":842.4556592932257,"p99":859.4324349273968}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:16+00","p50":688.52596875,"p95":783.0348659839834,"p99":811.3496807442837}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:17+00","p50":107.8342115,"p95":790.8719961222477,"p99":810.0910971106085}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:18+00","p50":116.53781625,"p95":842.2719780785284,"p99":866.1559901824003}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:19+00","p50":109.49712650000001,"p95":830.5998499945507,"p99":1371.32992618326}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:20+00","p50":107.783764125,"p95":944.7021668480029,"p99":966.5568989027722}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:21+00","p50":113.284234,"p95":889.4583924123841,"p99":1134.6218309054136}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:22+00","p50":616.5451075,"p95":797.8982933095185,"p99":815.9537981134291}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:23+00","p50":81.28988600000001,"p95":746.4988313239768,"p99":773.7183676869099}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:24+00","p50":694.863818,"p95":878.1996441259735,"p99":1449.411465187606}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:25+00","p50":661.0638879999999,"p95":775.635402378582,"p99":839.1896918192979}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:26+00","p50":763.98808,"p95":831.732323253232,"p99":1414.3224035469007}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:27+00","p50":723.8197727500001,"p95":839.6089844563531,"p99":849.5899957635671}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:28+00","p50":116.77925975,"p95":834.5510687159983,"p99":1543.5683662105992}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:29+00","p50":117.526399,"p95":802.0372145929737,"p99":827.9596948377833}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:30+00","p50":100.54473449999999,"p95":722.7150645335574,"p99":1400.025624940358}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:31+00","p50":640.1577785,"p95":697.1623824238311,"p99":703.2957118418021}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:32+00","p50":569.60793725,"p95":697.1630396883111,"p99":734.7984799269285}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:33+00","p50":92.32119875,"p95":715.0479155635971,"p99":747.845587508542}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:34+00","p50":727.11977925,"p95":808.8093907659426,"p99":1372.0464172528175}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:35+00","p50":638.611565,"p95":807.3771423745794,"p99":1465.9081902214546}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:36+00","p50":65.73843174999999,"p95":928.3932908793736,"p99":944.7086408448639}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:37+00","p50":616.4957522499999,"p95":908.5507952647705,"p99":936.05388623559}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:38+00","p50":104.761835875,"p95":996.1585314432125,"p99":1025.8025885197446}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:39+00","p50":116.222094,"p95":971.7074018833857,"p99":984.8917383078499}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:40+00","p50":87.93687600000001,"p95":854.9437290024647,"p99":874.0998325691106}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:41+00","p50":101.900605875,"p95":863.5622917634469,"p99":880.2251084084671}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:42+00","p50":747.4949622500001,"p95":879.3116484197096,"p99":1573.4635480084544}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:43+00","p50":443.2535735,"p95":843.6001929678047,"p99":1496.1132926844082}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:44+00","p50":103.668414,"p95":821.9479759732315,"p99":1437.5486519893986}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:45+00","p50":104.86000812500001,"p95":814.4674236040897,"p99":839.5542847080037}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:46+00","p50":656.032231,"p95":860.3183161102281,"p99":1518.5749786996807}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:47+00","p50":817.4430205,"p95":938.8320213186998,"p99":1619.3745474164095}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:48+00","p50":874.0658080000001,"p95":990.5876245271918,"p99":997.2471356679052}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:49+00","p50":758.041089,"p95":955.5084377954906,"p99":1438.0683047343932}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:50+00","p50":662.112320625,"p95":797.1309638544161,"p99":819.7984843161178}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:51+00","p50":683.8425952499999,"p95":788.0291592132695,"p99":1331.1540936484555}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:52+00","p50":666.46029875,"p95":815.2781762528855,"p99":1431.0292552063677}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:53+00","p50":769.495103,"p95":868.8312642688159,"p99":1542.5612955548627}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:54+00","p50":295.46924125,"p95":832.7103785882223,"p99":853.9305186710586}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:55+00","p50":123.652789625,"p95":702.6480259373322,"p99":1179.1907344160447}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:56+00","p50":571.088818,"p95":737.7508280802962,"p99":902.6227154990067}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:57+00","p50":163.88361849999998,"p95":557.4004367088044,"p99":1045.0805537616402}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:58+00","p50":138.91648849999999,"p95":623.0931183867741,"p99":673.3206044825828}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:45:59+00","p50":75.11913737500001,"p95":695.7670414780904,"p99":1223.1716759039873}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:00+00","p50":85.46271025,"p95":752.4541330829762,"p99":758.8844850142317}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:01+00","p50":96.9674445,"p95":715.6527295384142,"p99":732.3697721707562}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:02+00","p50":68.5710335,"p95":785.4231087829194,"p99":816.5957909644603}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:03+00","p50":600.3258519999999,"p95":754.5164500125784,"p99":812.4384835239601}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:04+00","p50":220.19396925,"p95":764.074723689581,"p99":783.6199675725229}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:05+00","p50":86.5598095,"p95":747.5866568598074,"p99":1394.8373230738573}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:06+00","p50":387.930116,"p95":824.2238085461345,"p99":1422.2440268075122}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:07+00","p50":655.4138634999999,"p95":808.9175085380936,"p99":819.9152349475289}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:08+00","p50":562.660739,"p95":749.6713379817321,"p99":894.0423545453721}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:09+00","p50":744.8099485,"p95":835.7262630666952,"p99":1507.0780469088897}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:10+00","p50":738.593509,"p95":824.7698238240329,"p99":844.7158693405667}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:11+00","p50":588.31341225,"p95":775.4465699183452,"p99":1282.4373771364344}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:12+00","p50":242.01334150000002,"p95":787.9302633854161,"p99":1278.1143525060786}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:13+00","p50":82.9306425,"p95":824.3198081137468,"p99":1322.8774543290301}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:14+00","p50":83.5553775,"p95":815.1334368751171,"p99":1455.1966413230143}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:15+00","p50":671.747295,"p95":858.0320427440549,"p99":1354.807620901456}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:16+00","p50":699.857814625,"p95":796.1410200463614,"p99":1492.2283868043228}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:17+00","p50":96.899547,"p95":871.9100075593578,"p99":1435.5541638533552}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:18+00","p50":637.92791375,"p95":790.1056449591965,"p99":804.3668659511566}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:19+00","p50":601.465368625,"p95":775.2521794834665,"p99":1318.9221550740283}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:20+00","p50":99.1735995,"p95":751.3207711684593,"p99":791.2067172721763}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:21+00","p50":96.675345875,"p95":810.3878734536066,"p99":1402.0698562201305}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:22+00","p50":107.38077799999999,"p95":759.3091611155386,"p99":775.8024819466953}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:23+00","p50":140.255932,"p95":697.7769375465653,"p99":1186.371807908178}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:24+00","p50":563.583800875,"p95":759.4315956733574,"p99":779.3285183312657}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:25+00","p50":141.49609700000002,"p95":605.6897342542419,"p99":1199.7610601891402}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:26+00","p50":119.09713049999999,"p95":666.9193553067882,"p99":740.0533319055186}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:27+00","p50":299.94233899999995,"p95":755.7476397948903,"p99":766.544200806315}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:28+00","p50":615.568062875,"p95":725.2923650970461,"p99":1367.9528970422175}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:29+00","p50":501.53985124999997,"p95":594.8824950281698,"p99":1090.5488719160032}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:30+00","p50":550.074116,"p95":703.7024713378715,"p99":1172.2559080063315}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:31+00","p50":443.3620095,"p95":541.2813468280783,"p99":878.1505472881489}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:32+00","p50":91.98802,"p95":669.0292171711002,"p99":888.7684250311031}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:33+00","p50":536.811613125,"p95":696.8048871795861,"p99":1139.282266761349}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:34+00","p50":599.29383575,"p95":714.7003122115418,"p99":1258.954391068933}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:35+00","p50":522.2459495,"p95":658.8783217851059,"p99":674.4149549244972}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:36+00","p50":95.91266949999999,"p95":691.0073433583452,"p99":1143.2555296529456}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:37+00","p50":621.9141595,"p95":699.8770973847377,"p99":744.9771129487686}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:38+00","p50":104.391755,"p95":734.6553045711778,"p99":786.7984468761097}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:39+00","p50":78.43131749999999,"p95":853.0549331895018,"p99":1557.352382182696}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:40+00","p50":713.119144,"p95":872.3805335340634,"p99":1572.3498037637482}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:41+00","p50":693.7826607500001,"p95":806.6288770415865,"p99":1444.2764181607358}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:42+00","p50":744.28496325,"p95":872.5478150452229,"p99":896.3830859839079}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:43+00","p50":147.81662525000002,"p95":818.1277334826766,"p99":888.2068185897377}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:44+00","p50":571.334157,"p95":652.3805408522917,"p99":663.1077606800566}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:45+00","p50":119.14563749999999,"p95":765.2784675429282,"p99":777.5126492326089}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:46+00","p50":473.55428225,"p95":769.2300093019625,"p99":1458.3460274411352}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:47+00","p50":116.103188375,"p95":628.8671917600609,"p99":654.1473500599377}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:48+00","p50":210.27037475,"p95":749.8847463606764,"p99":842.0038304443756}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:49+00","p50":632.573099375,"p95":718.3967519588701,"p99":743.4671695906488}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:50+00","p50":459.38801824999996,"p95":680.4053460796831,"p99":1260.082090199358}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:51+00","p50":514.877943375,"p95":620.3114213767221,"p99":704.1372322328984}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:52+00","p50":445.53663125,"p95":568.237054784164,"p99":931.7686444224324}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:53+00","p50":393.5033145,"p95":497.65686670969154,"p99":845.809881194355}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:54+00","p50":296.21919,"p95":458.6683174573641,"p99":749.2839294546366}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:55+00","p50":287.17872887500005,"p95":382.21576987979034,"p99":472.72862589892935}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:56+00","p50":398.8376715,"p95":494.031165883492,"p99":759.6052434576554}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:57+00","p50":433.47860975,"p95":496.64983857380054,"p99":864.2718659344063}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:58+00","p50":353.83440925,"p95":470.50095258743573,"p99":646.9075284398842}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:46:59+00","p50":268.78671175,"p95":453.97477488113543,"p99":494.042487192317}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:00+00","p50":169.60384525,"p95":585.7001353093164,"p99":608.1741824463929}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:01+00","p50":433.66588924999996,"p95":651.5389201731483,"p99":1168.0096439201318}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:02+00","p50":114.691527125,"p95":633.2042487452314,"p99":647.2442904679723}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:03+00","p50":115.33051187500001,"p95":615.9186288518791,"p99":1113.0686916127459}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:04+00","p50":84.0653475,"p95":685.9152588341708,"p99":1200.9940932294048}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:05+00","p50":595.2619445,"p95":767.4353087385167,"p99":1263.365097162118}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:06+00","p50":623.7569832500001,"p95":849.5130697790872,"p99":1350.7247930038848}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:07+00","p50":120.498008,"p95":802.7428680151816,"p99":1460.6263782823703}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:08+00","p50":95.567414125,"p95":761.7750743313234,"p99":768.6201903216519}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:09+00","p50":595.427563125,"p95":832.5773960302434,"p99":1365.8852925794238}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:10+00","p50":115.759408875,"p95":673.2052176371664,"p99":695.8486761165066}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:11+00","p50":82.515091375,"p95":674.9812851322604,"p99":1068.9637434811114}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:12+00","p50":578.435672875,"p95":737.242101488435,"p99":1288.2537798161034}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:13+00","p50":643.106219,"p95":782.3824376498073,"p99":1355.285669870819}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:14+00","p50":101.62573162499999,"p95":757.3006189474809,"p99":781.3604936933605}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:15+00","p50":115.9472585,"p95":789.5953716125445,"p99":819.1870230710501}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:16+00","p50":116.89676949999999,"p95":662.3854929652628,"p99":1202.1438368951683}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:17+00","p50":80.28190000000001,"p95":719.2535055489619,"p99":1230.7422494297814}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:18+00","p50":536.96540875,"p95":747.9567612489409,"p99":1343.2836187939358}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:19+00","p50":103.37318775,"p95":669.1000824948275,"p99":1114.936781253652}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:20+00","p50":608.3634832500001,"p95":698.8214207666379,"p99":1132.7459026683625}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:21+00","p50":570.7475704999999,"p95":693.2945956845451,"p99":713.3422798048516}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:22+00","p50":97.66945612500001,"p95":665.1729531339365,"p99":1085.3988397469288}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:23+00","p50":97.2767315,"p95":669.521162371227,"p99":754.8304644977798}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:24+00","p50":109.4852255,"p95":769.226698482621,"p99":784.3624675670079}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:25+00","p50":128.41331474999998,"p95":624.6457007438178,"p99":650.5311066878462}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:26+00","p50":509.057724,"p95":647.3652417942295,"p99":654.5620322992515}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:27+00","p50":240.7177795,"p95":573.367714555741,"p99":623.5318395399356}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:28+00","p50":113.076564,"p95":658.4665549807131,"p99":709.1253027357765}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:29+00","p50":82.74750287500001,"p95":652.5855876613992,"p99":713.4283714277752}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:30+00","p50":553.005902,"p95":651.2162006346664,"p99":667.8298570509721}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:31+00","p50":651.494776,"p95":714.2459401573581,"p99":727.0816246756516}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:32+00","p50":575.690414875,"p95":761.6401511211648,"p99":1336.3385551777894}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:33+00","p50":634.9293575000002,"p95":801.5568306412588,"p99":810.4369350011143}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:34+00","p50":112.43761075,"p95":722.7541257472262,"p99":1129.3551196086883}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:35+00","p50":93.28392675,"p95":755.5850497288093,"p99":768.3332124174159}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:36+00","p50":67.885473625,"p95":882.2562893653511,"p99":892.503656662961}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:37+00","p50":705.4814974999999,"p95":820.227570021609,"p99":839.3941409797283}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:38+00","p50":141.54974900000002,"p95":786.2203973325583,"p99":850.9326722749481}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:39+00","p50":492.11495475000004,"p95":821.0086441646683,"p99":833.7416292054543}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:40+00","p50":763.3627135,"p95":855.3781406302147,"p99":922.3851257867447}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:41+00","p50":238.4347105,"p95":803.3062607974837,"p99":1438.4339078753062}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:42+00","p50":788.7190625000001,"p95":864.5141713044662,"p99":882.0696843567581}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:43+00","p50":716.594246,"p95":797.6324964362476,"p99":817.6932912032222}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:44+00","p50":109.99552824999999,"p95":751.4949949349082,"p99":767.9221400817552}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:45+00","p50":93.5002795,"p95":798.5637145100688,"p99":804.9697349355088}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:46+00","p50":417.05654925,"p95":844.8749054834979,"p99":1454.9001005208572}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:47+00","p50":387.75186349999996,"p95":765.5424031796722,"p99":817.9698935651599}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:48+00","p50":104.464174,"p95":842.788295473869,"p99":1032.0937582941137}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:49+00","p50":723.2296405,"p95":902.4163616292919,"p99":1555.7967481279502}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:50+00","p50":629.8915382499999,"p95":728.2986218643617,"p99":1431.8040030166394}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:51+00","p50":479.9069055,"p95":704.8384651550514,"p99":719.810380112854}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:52+00","p50":123.731581,"p95":679.2318008618396,"p99":691.4896535818484}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:53+00","p50":643.8250945,"p95":711.1966533549271,"p99":1294.629456621628}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:54+00","p50":688.71478475,"p95":757.7519323557692,"p99":796.9192032744961}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:55+00","p50":671.8456285,"p95":737.7910522955754,"p99":757.8293564560056}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:56+00","p50":553.792288,"p95":747.9470209500446,"p99":761.830817231491}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:57+00","p50":380.879904,"p95":624.4906196357266,"p99":1111.5014270743523}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:58+00","p50":465.4566065,"p95":602.2510905951186,"p99":901.760303634018}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:47:59+00","p50":112.9543205,"p95":507.24202709089093,"p99":527.5508170252232}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:00+00","p50":80.04090249999999,"p95":618.0598821036658,"p99":964.2528214625147}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:01+00","p50":549.682426,"p95":621.0470596738325,"p99":639.3599464962197}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:02+00","p50":124.88856475,"p95":620.6035018473634,"p99":632.0555472665868}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:03+00","p50":550.1441505,"p95":612.4393516211327,"p99":1146.3857234712382}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:04+00","p50":412.88055175,"p95":535.6977218952914,"p99":1010.1618266050548}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:05+00","p50":241.89805124999998,"p95":480.0294454990117,"p99":565.0077671132307}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:06+00","p50":309.19938975,"p95":557.2593080720204,"p99":925.1408223710914}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:07+00","p50":200.35981700000002,"p95":412.30378318191526,"p99":443.3758270422058}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:08+00","p50":214.521426,"p95":489.1687230628362,"p99":501.27048755811194}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:09+00","p50":487.6523345,"p95":592.5596788530843,"p99":631.4581933139525}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:10+00","p50":417.534031625,"p95":650.9490510904255,"p99":1142.4780949960077}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:11+00","p50":434.37756425000003,"p95":511.5344372832643,"p99":835.8355497361753}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:12+00","p50":277.31485387500004,"p95":538.3734151248196,"p99":554.0274798432184}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:13+00","p50":371.57046325,"p95":478.0362634475234,"p99":801.7308900793934}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:14+00","p50":311.8103705,"p95":356.9247454015274,"p99":511.34595991748046}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:15+00","p50":335.35749024999996,"p95":415.6892834882769,"p99":631.0998042430593}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:16+00","p50":274.6000905,"p95":389.1789680032361,"p99":430.79315238157795}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:17+00","p50":221.872556,"p95":455.55253670266364,"p99":783.6193724553285}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:18+00","p50":304.781111,"p95":451.45092772065414,"p99":534.336403091614}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:19+00","p50":327.1428465,"p95":381.1703145246973,"p99":632.626106252307}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:20+00","p50":273.30005025,"p95":411.7953636148255,"p99":435.5471754613066}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:21+00","p50":239.984005,"p95":605.5067839155705,"p99":624.6066701734604}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:22+00","p50":150.8576655,"p95":642.4235452280884,"p99":1027.0999443562011}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:23+00","p50":498.670859,"p95":705.3525027979546,"p99":762.2698651318865}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:24+00","p50":212.95178699999997,"p95":768.703919353083,"p99":784.0174459319525}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:25+00","p50":76.52326024999999,"p95":706.43353968445,"p99":1119.8985452528439}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:26+00","p50":83.245035875,"p95":757.5051178608909,"p99":828.761476679544}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:27+00","p50":314.20786062499997,"p95":806.6409730093827,"p99":1425.6951374775135}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:28+00","p50":709.5339796249999,"p95":814.5006095846198,"p99":1426.2617682852258}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:29+00","p50":113.454402625,"p95":732.4878557793858,"p99":748.8627067535674}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:30+00","p50":660.839477625,"p95":773.3054731302905,"p99":1346.2778358920905}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:31+00","p50":337.664898625,"p95":801.3159101775608,"p99":1393.9091387091944}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:32+00","p50":90.363887,"p95":786.340821278738,"p99":864.7210164364586}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:33+00","p50":72.60304,"p95":971.2812440268616,"p99":1113.8249271221514}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:34+00","p50":777.99377,"p95":985.8236826422706,"p99":998.9018441103477}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:35+00","p50":121.797462,"p95":806.6025213336068,"p99":1319.9738068045197}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:36+00","p50":136.69370350000003,"p95":857.2430621072092,"p99":1496.4879467178714}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:37+00","p50":118.815705125,"p95":815.9977423631732,"p99":849.6572761848975}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:38+00","p50":89.691594375,"p95":902.4679385172279,"p99":927.1797757571921}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:39+00","p50":103.38175325,"p95":886.8165088323526,"p99":1629.5776352622556}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:40+00","p50":98.8685985,"p95":936.0702848264666,"p99":1643.5831368246784}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:41+00","p50":455.08753575000003,"p95":918.738713649706,"p99":1724.4437055268963}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:42+00","p50":108.00764025000001,"p95":934.7884516733433,"p99":1685.575428011436}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:43+00","p50":90.538017,"p95":994.1536193502162,"p99":1062.3122429377495}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:44+00","p50":95.741310125,"p95":1045.2883064481073,"p99":1654.3001958400102}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:45+00","p50":303.0098805,"p95":983.8245739853796,"p99":1785.9007499829893}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:46+00","p50":110.78929525,"p95":1045.4875890924218,"p99":1768.191571338608}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:47+00","p50":113.16953137499999,"p95":1040.261707651156,"p99":1064.816347309103}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:48+00","p50":119.808087,"p95":1146.75371270385,"p99":1156.1874010955921}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:49+00","p50":940.4759545,"p95":1082.0757913894308,"p99":2030.596992008389}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:50+00","p50":903.62399675,"p95":974.0854127833504,"p99":1908.9599463229993}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:51+00","p50":967.9986825,"p95":1014.0176219769993,"p99":1244.7300004167882}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:52+00","p50":1021.00771175,"p95":1122.9150658056135,"p99":1227.9024109093382}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:53+00","p50":273.63448175,"p95":1026.6458572614372,"p99":1967.4131494413602}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:54+00","p50":101.86835825,"p95":1022.2227484962663,"p99":1648.58621036986}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:55+00","p50":103.42359675,"p95":1051.0772486667995,"p99":1075.4278838843761}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:56+00","p50":104.61576525000001,"p95":1079.5869794184482,"p99":1919.2970786996605}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:57+00","p50":772.60297,"p95":1086.216817286973,"p99":1105.3445035775242}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:58+00","p50":841.960704375,"p95":1043.7652286652446,"p99":1944.7447328844664}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:48:59+00","p50":626.741469,"p95":925.9409968273542,"p99":1872.8324781313677}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:00+00","p50":129.9908565,"p95":716.3045079260139,"p99":1288.739318709198}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:01+00","p50":103.53098162500001,"p95":682.6611790541072,"p99":721.5263661444963}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:02+00","p50":78.07176212499999,"p95":685.7117936245966,"p99":1243.1543442226114}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:03+00","p50":688.9759425,"p95":789.1339182396518,"p99":818.2144520472523}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:04+00","p50":556.945023625,"p95":931.2583120983447,"p99":944.8767448625664}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:05+00","p50":118.202837375,"p95":820.2989282155556,"p99":837.3513568308093}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:06+00","p50":669.164102625,"p95":783.3168688180198,"p99":1445.207574363511}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:07+00","p50":104.3986405,"p95":725.6241283809321,"p99":1267.5848221871106}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:08+00","p50":84.32877475,"p95":894.9770698771714,"p99":1454.9902229020968}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:09+00","p50":85.25501425,"p95":875.3295487135858,"p99":1590.2995779503822}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:10+00","p50":88.018027875,"p95":968.883953524386,"p99":989.7744239230609}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:11+00","p50":80.609232125,"p95":977.7881077553669,"p99":1854.7314415528851}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:12+00","p50":73.74759999999999,"p95":1031.7739584044364,"p99":1067.8899105748283}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:13+00","p50":882.24266975,"p95":959.5021686971838,"p99":993.1670179892874}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:14+00","p50":793.875368875,"p95":907.8905559107,"p99":1702.2182781260533}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:15+00","p50":775.3709235,"p95":861.2567545245771,"p99":1618.4889023017474}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:16+00","p50":629.635977375,"p95":758.448924247446,"p99":789.6678254292235}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:17+00","p50":612.08785475,"p95":672.4229918159779,"p99":1248.3076851605242}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:18+00","p50":701.824208,"p95":872.1463574468893,"p99":893.9567875690518}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:19+00","p50":120.94848675,"p95":818.1691770692132,"p99":868.935201611999}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:20+00","p50":94.27102349999998,"p95":777.899137130891,"p99":949.7966478330638}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:21+00","p50":83.100668,"p95":875.4234619999726,"p99":1527.5823871087732}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:22+00","p50":108.4198375,"p95":897.193142616882,"p99":1551.5683504057508}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:23+00","p50":94.682036125,"p95":860.615358750777,"p99":880.3414434699331}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:24+00","p50":768.994486625,"p95":876.2353815581004,"p99":1543.2590623884018}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:25+00","p50":781.7667166250001,"p95":929.9063780386118,"p99":1661.4006397330004}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:26+00","p50":745.51115525,"p95":1028.3643662159648,"p99":1716.9370744792993}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:27+00","p50":869.021291125,"p95":1046.6775355226087,"p99":1863.9796564143833}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:28+00","p50":832.08738425,"p95":889.4999674422668,"p99":1755.1346835112713}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:29+00","p50":127.93300062499999,"p95":804.2020337532523,"p99":823.8369804663682}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:30+00","p50":100.988033875,"p95":718.9385389995988,"p99":736.3277949770245}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:31+00","p50":94.85565875,"p95":764.8988084608467,"p99":794.9306844778405}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:32+00","p50":109.87539025000001,"p95":843.2342138568671,"p99":856.5836337987118}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:33+00","p50":530.54472425,"p95":736.5644176611887,"p99":770.2404992375574}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:34+00","p50":652.12492925,"p95":742.5413130316205,"p99":931.8925453907099}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:35+00","p50":111.6113925,"p95":755.8804887268492,"p99":771.2037322440224}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:36+00","p50":116.23973249999999,"p95":579.4770872289279,"p99":1082.8913469387821}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:37+00","p50":519.252801375,"p95":585.8841791381333,"p99":1076.58170186686}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:38+00","p50":582.923061,"p95":750.020758578925,"p99":774.7652954837117}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:39+00","p50":129.834048,"p95":793.8781212120749,"p99":1389.3344195340615}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:40+00","p50":586.9657627500001,"p95":670.0775303004543,"p99":1330.8206683440324}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:41+00","p50":617.31109375,"p95":748.1348105218342,"p99":1286.2253439344684}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:42+00","p50":671.0071244999999,"p95":732.5782327336553,"p99":1413.2019456798243}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:43+00","p50":387.06797875,"p95":829.1163573002138,"p99":857.2229952899771}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:44+00","p50":86.22922187500001,"p95":820.0516209151207,"p99":871.32299781712}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:45+00","p50":95.651719,"p95":733.0425748920916,"p99":760.6874874350748}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:46+00","p50":105.331855375,"p95":782.0578766796625,"p99":788.1293955987642}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:47+00","p50":125.91306300000001,"p95":674.1390620615608,"p99":719.3207408405062}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:48+00","p50":133.628932,"p95":756.9878561028617,"p99":768.321995678063}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:49+00","p50":239.2291515,"p95":696.0873804757553,"p99":709.1846221618766}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:50+00","p50":393.924321,"p95":631.7150668109515,"p99":978.0905942111605}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:51+00","p50":100.1350985,"p95":747.1028147213444,"p99":892.087395637446}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:52+00","p50":80.119784,"p95":895.3604203240058,"p99":906.2120763899293}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:53+00","p50":295.458426,"p95":816.0768729679575,"p99":1554.9800333060602}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:54+00","p50":86.04985150000002,"p95":659.1101894489277,"p99":671.0210983434475}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:55+00","p50":89.37912925,"p95":658.44207098605,"p99":1119.5332424066096}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:56+00","p50":112.025921125,"p95":781.500458887154,"p99":880.6413599011006}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:57+00","p50":125.862802,"p95":800.6059248873784,"p99":1387.020049992745}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:58+00","p50":100.4748585,"p95":1083.7542059954983,"p99":1110.311005322431}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:49:59+00","p50":791.1529419999999,"p95":1052.0417307379262,"p99":1709.1649410904686}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:00+00","p50":712.5176792499999,"p95":901.6490778713837,"p99":1650.6453527641906}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:01+00","p50":642.551613125,"p95":829.5267964778093,"p99":1484.4243326289532}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:02+00","p50":104.749961,"p95":903.1327824304748,"p99":920.0659723173409}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:03+00","p50":95.05495475,"p95":929.1321843626488,"p99":953.9353256387162}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:04+00","p50":280.56991849999997,"p95":934.7329895787964,"p99":1740.910626194353}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:05+00","p50":93.43842375,"p95":948.9514862967563,"p99":1770.7453524620057}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:06+00","p50":293.53731025,"p95":1009.7710532106111,"p99":1731.7955473814268}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:07+00","p50":776.9651105,"p95":967.9636252624828,"p99":1811.1438290942115}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:08+00","p50":640.83667625,"p95":925.9022368907686,"p99":1676.8665601124574}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:09+00","p50":789.4476215,"p95":952.6196558856735,"p99":1738.4342859717065}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:10+00","p50":714.820739,"p95":841.9031254251247,"p99":1651.9084097411326}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:11+00","p50":521.442563375,"p95":705.3171632144696,"p99":1324.7176894864026}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:12+00","p50":639.515801,"p95":700.7458547481866,"p99":1278.4302566953083}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:13+00","p50":683.3057105,"p95":792.5292455909988,"p99":1383.5130779524115}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:14+00","p50":731.87380375,"p95":820.3643402774895,"p99":1371.5457682702904}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:15+00","p50":308.214945875,"p95":800.1721363983312,"p99":832.7435022536043}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:16+00","p50":95.41449850000001,"p95":621.8878171771916,"p99":679.4292155636172}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:17+00","p50":93.949568375,"p95":768.0138640381915,"p99":824.3791715645575}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:18+00","p50":110.594532,"p95":854.4987153627155,"p99":867.0998075665019}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:19+00","p50":586.774215875,"p95":817.3130459946867,"p99":1342.5506660928468}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:20+00","p50":557.7182339999999,"p95":630.9995098405986,"p99":650.2650402873763}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:21+00","p50":87.202889,"p95":713.8587155674644,"p99":1168.7471971354141}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:22+00","p50":640.52665725,"p95":793.1996607872419,"p99":1390.6978716095784}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:23+00","p50":132.26946325,"p95":648.3281488028521,"p99":951.6345693251674}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:24+00","p50":160.08162149999998,"p95":637.6088811832724,"p99":1057.3107655371628}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:25+00","p50":192.26016375,"p95":524.9245420190991,"p99":850.469291903554}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:26+00","p50":381.78417375000004,"p95":521.5879732593357,"p99":875.6462896975136}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:27+00","p50":221.51610449999998,"p95":487.5844254232818,"p99":536.1802740206441}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:28+00","p50":210.953463,"p95":610.7389170261557,"p99":1013.0256482378893}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:29+00","p50":128.33694225,"p95":573.9862548539568,"p99":1002.696724465806}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:30+00","p50":215.7355205,"p95":650.2160672219801,"p99":1132.7537701321573}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:31+00","p50":457.25359549999996,"p95":605.7403284520896,"p99":1090.78744208395}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:32+00","p50":367.228019875,"p95":470.5058107788862,"p99":781.8830233559444}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:33+00","p50":375.24030925,"p95":415.99723371247865,"p99":444.38061062507245}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:34+00","p50":304.921029,"p95":382.23104071868323,"p99":510.26780253876115}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:35+00","p50":297.39690225000004,"p95":377.4282703921096,"p99":603.6702991270165}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:36+00","p50":276.732439625,"p95":353.94692497382397,"p99":405.9969165544574}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:37+00","p50":234.69498299999998,"p95":429.04911144353986,"p99":473.0028015145616}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:38+00","p50":438.86642475,"p95":560.9319316062794,"p99":929.5993257683983}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:39+00","p50":357.96543925000003,"p95":438.9944021965405,"p99":882.039756235819}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:40+00","p50":283.47329275000004,"p95":355.9644332269621,"p99":505.1557103795352}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:41+00","p50":241.969093,"p95":432.85629332013133,"p99":441.96072623747256}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:42+00","p50":239.978108625,"p95":425.46725042164263,"p99":659.6673333214183}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:43+00","p50":443.416979,"p95":537.754894225896,"p99":893.2604973602323}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:44+00","p50":234.1475795,"p95":481.4766555449567,"p99":669.8892911808467}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:45+00","p50":175.161103,"p95":469.23364054244325,"p99":487.5382492383871}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:46+00","p50":458.79505887499994,"p95":535.436782755597,"p99":559.6641022932832}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:47+00","p50":502.7074555,"p95":607.334698165794,"p99":1064.5740103929543}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:48+00","p50":447.339092625,"p95":562.1761792779944,"p99":892.6157066755934}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:49+00","p50":316.553235,"p95":560.4792626692589,"p99":589.5094885692815}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:50+00","p50":202.340657125,"p95":521.2590574251797,"p99":740.3048775340875}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:51+00","p50":440.115324,"p95":542.2094039367599,"p99":848.7164114582824}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:52+00","p50":324.48566074999997,"p95":432.6526184308235,"p99":454.2102072144544}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:53+00","p50":290.4834675,"p95":389.43885680308125,"p99":495.5632919055853}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:54+00","p50":390.39373,"p95":513.3351291002074,"p99":542.8290430029107}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:55+00","p50":357.56969749999996,"p95":450.46677656632596,"p99":468.1591685423641}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:56+00","p50":487.870816,"p95":570.4745929658434,"p99":729.8772204909727}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:57+00","p50":400.71176575000004,"p95":546.3358515805907,"p99":559.845250252912}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:58+00","p50":376.00485737500003,"p95":586.7317079793031,"p99":976.6037737740812}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:50:59+00","p50":364.00851475,"p95":480.68783147193386,"p99":532.3782379675159}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:00+00","p50":284.85761249999996,"p95":474.15282371866715,"p99":492.7574578919909}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:01+00","p50":203.67234975000002,"p95":564.5914215489541,"p99":740.320976739527}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:02+00","p50":578.753872,"p95":652.7238223906638,"p99":1106.2095207487898}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:03+00","p50":491.449663,"p95":626.676619027724,"p99":650.4427486659785}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:04+00","p50":84.847254125,"p95":645.9125586179349,"p99":665.5865355649929}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:05+00","p50":89.641759,"p95":604.0079675356607,"p99":955.8840469553625}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:06+00","p50":106.11380750000001,"p95":653.1685827976827,"p99":720.1897306562996}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:07+00","p50":601.6485102500001,"p95":653.1006091412737,"p99":702.9452480035716}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:08+00","p50":621.8143556250001,"p95":704.354180508445,"p99":721.3159088788143}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:09+00","p50":115.97206175,"p95":603.1013242168254,"p99":1062.419569763422}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:10+00","p50":112.07383949999999,"p95":700.4719116361597,"p99":1203.261291899008}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:11+00","p50":597.950281625,"p95":758.8605716789333,"p99":1216.8859200086463}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:12+00","p50":589.135096,"p95":662.1355080637982,"p99":687.3708392935217}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:13+00","p50":467.609732,"p95":682.8946792657875,"p99":1130.4847196145645}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:14+00","p50":95.845928,"p95":841.6512347642516,"p99":856.2555189822725}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:15+00","p50":716.670476375,"p95":848.6797282963354,"p99":894.9695307906885}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:16+00","p50":88.424713,"p95":871.0218539102978,"p99":1413.0790843765997}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:17+00","p50":792.926725125,"p95":858.4841201329043,"p99":1544.2263243713194}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:18+00","p50":724.523301125,"p95":950.4331357209846,"p99":1694.8538334546533}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:19+00","p50":762.7704309999999,"p95":848.9031410275078,"p99":1473.7004894261302}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:20+00","p50":436.41407425,"p95":853.5649370082307,"p99":1316.0156547264983}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:21+00","p50":115.52632650000001,"p95":1017.3798492774257,"p99":1560.5826853904284}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:22+00","p50":109.654862,"p95":1026.6562105824044,"p99":1269.3856761200598}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:23+00","p50":805.9244457499999,"p95":979.9064123737305,"p99":1775.4783727044928}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:24+00","p50":590.560384375,"p95":860.6288774120189,"p99":1543.604587833174}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:25+00","p50":120.74508700000001,"p95":859.9307839291931,"p99":877.426450322774}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:26+00","p50":95.85280037499999,"p95":914.351248910777,"p99":1164.5171578402922}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:27+00","p50":91.77649325,"p95":887.6663869858046,"p99":1575.6873313422454}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:28+00","p50":97.5336005,"p95":901.9856477215706,"p99":1003.9006604586659}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:29+00","p50":101.17215299999998,"p95":864.4445763485127,"p99":1305.4054632400178}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:30+00","p50":119.174347375,"p95":704.8971680416506,"p99":731.24540573777}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:31+00","p50":614.6874383749999,"p95":724.9634100659467,"p99":1208.4942165697498}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:32+00","p50":502.60443000000004,"p95":843.3162884190166,"p99":1412.7628362798541}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:33+00","p50":104.301880625,"p95":687.9666589281217,"p99":704.7006019180035}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:34+00","p50":556.8220650000001,"p95":713.1945360845747,"p99":809.6985074163026}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:35+00","p50":667.10109975,"p95":741.938805730253,"p99":1208.3030852669624}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:36+00","p50":204.900803,"p95":642.7304509545784,"p99":1180.2354377952117}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:37+00","p50":612.27608625,"p95":761.3757450736545,"p99":1126.4836966918192}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:38+00","p50":572.23982525,"p95":680.1928231431449,"p99":692.6061800202725}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:39+00","p50":567.52986975,"p95":685.3349149288565,"p99":1210.1249897335094}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:40+00","p50":91.21434887499998,"p95":820.4352738082716,"p99":1138.129445420957}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:41+00","p50":77.126399625,"p95":784.2686345599245,"p99":1434.3195155966143}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:42+00","p50":106.559587,"p95":810.1408759237785,"p99":819.8384447997437}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:43+00","p50":103.274454125,"p95":906.7493627593351,"p99":951.1839114554339}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:44+00","p50":86.24355650000001,"p95":729.6120354518317,"p99":1325.9796870868345}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:45+00","p50":61.203374749999995,"p95":835.4608001256543,"p99":1376.4650406365968}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:46+00","p50":781.08166625,"p95":839.7331106899882,"p99":1483.8319265741045}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:47+00","p50":755.288991,"p95":872.6853586033602,"p99":1343.5185451792868}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:48+00","p50":710.4827115,"p95":835.3523106040394,"p99":851.2215841598917}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:49+00","p50":633.313571875,"p95":745.3368832239697,"p99":1347.6717572171237}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:50+00","p50":73.26731000000001,"p95":793.0562070957142,"p99":862.0684880441546}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:51+00","p50":841.7003857500001,"p95":952.8737680721606,"p99":968.0073316063797}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:52+00","p50":87.43300125,"p95":945.8999975670786,"p99":1668.7991379628068}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:53+00","p50":120.69861875000001,"p95":929.8220639805113,"p99":951.38850317956}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:54+00","p50":101.74409025,"p95":972.6604067092297,"p99":1638.931432530853}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:55+00","p50":100.941593,"p95":971.6112074446755,"p99":1073.809679524137}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:56+00","p50":316.47652475,"p95":1061.868387017819,"p99":1892.9658508550112}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:57+00","p50":83.9081785,"p95":1115.8619023221588,"p99":1338.0331121994325}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:58+00","p50":624.777393625,"p95":1135.0439031415985,"p99":1147.2893825344377}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:51:59+00","p50":781.2051951249999,"p95":962.4038908975821,"p99":972.0937482887888}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:00+00","p50":98.91552300000001,"p95":972.297857029345,"p99":1007.693653640646}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:01+00","p50":852.432671,"p95":1053.7322229083147,"p99":1205.0193890502815}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:02+00","p50":982.306929,"p95":1077.5785987191339,"p99":1333.0898730502206}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:03+00","p50":804.9349441250001,"p95":1080.6965556084253,"p99":1094.04309885645}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:04+00","p50":507.68278050000004,"p95":1049.2938072939053,"p99":1799.559664216693}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:05+00","p50":910.9443576250001,"p95":1004.9389604814793,"p99":1026.2516288161403}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:06+00","p50":89.0554085,"p95":1035.097197781835,"p99":1739.2883349434858}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:07+00","p50":865.6858507500001,"p95":1013.5563311046512,"p99":1787.2684047078587}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:08+00","p50":733.307052,"p95":1124.0021750845149,"p99":1881.886651234497}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:09+00","p50":101.18553275,"p95":1067.0410368586777,"p99":1081.3992857233325}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:10+00","p50":95.369913,"p95":1052.742028241631,"p99":1902.4544310200324}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:11+00","p50":97.15598299999999,"p95":1071.3602919541759,"p99":1078.5568050640793}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:12+00","p50":94.82046299999999,"p95":1130.790219684693,"p99":1964.2463546441122}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:13+00","p50":111.3744765,"p95":1116.4732573611525,"p99":2111.75610551831}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:14+00","p50":849.47227075,"p95":960.8620291521106,"p99":1907.6904703619139}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:15+00","p50":119.262326,"p95":932.8277698012889,"p99":1021.8507423688643}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:16+00","p50":807.9974951249999,"p95":896.1268624988579,"p99":1660.0523029090025}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:17+00","p50":79.73973562500001,"p95":949.8041877210776,"p99":979.0895330914916}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:18+00","p50":101.2189575,"p95":1089.696898421404,"p99":1834.540829641899}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:19+00","p50":94.649149375,"p95":1100.4473658698791,"p99":1913.4683856910938}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:20+00","p50":928.400848125,"p95":1035.7688176160636,"p99":1044.0766817838112}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:21+00","p50":122.6555565,"p95":950.1385571013143,"p99":960.7365580899238}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:22+00","p50":836.67326625,"p95":957.2258155905294,"p99":1826.6737459801884}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:23+00","p50":850.18850325,"p95":936.8108292833635,"p99":1731.7052419506588}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:24+00","p50":116.61463025,"p95":927.222176775207,"p99":969.6687541186835}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:25+00","p50":592.2823142499999,"p95":812.1642124491866,"p99":1346.1808312423896}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:26+00","p50":132.6531505,"p95":929.1380102007276,"p99":1562.1603977837658}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:27+00","p50":816.8951837500001,"p95":948.848231035155,"p99":970.0761692273164}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:28+00","p50":783.343141625,"p95":899.9226733505767,"p99":923.5410027903895}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:29+00","p50":152.787154,"p95":783.8683387000899,"p99":1506.2255456420403}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:30+00","p50":105.70765075,"p95":679.503175514115,"p99":720.9709536053796}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:31+00","p50":91.929178,"p95":755.8812889675567,"p99":764.7222895973674}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:32+00","p50":70.310186,"p95":871.9637004138655,"p99":886.402786635592}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:33+00","p50":479.40871787500004,"p95":824.9852161265376,"p99":1581.0379635592062}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:34+00","p50":90.1207385,"p95":888.4773816964864,"p99":974.8345407538033}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:35+00","p50":87.2650235,"p95":948.8940794178451,"p99":959.4873322918605}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:36+00","p50":447.94354150000004,"p95":913.6866523138384,"p99":1597.3458856856419}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:37+00","p50":646.8611275000001,"p95":765.3920434175959,"p99":1262.8876526845113}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:38+00","p50":372.172393,"p95":875.6318568318162,"p99":1398.6784930379954}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:39+00","p50":745.289261625,"p95":870.0140138492844,"p99":1486.2860874695502}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:40+00","p50":675.857412625,"p95":758.9112674551751,"p99":1306.3148468059317}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:41+00","p50":398.23661,"p95":804.00092756659,"p99":907.8365080460038}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:42+00","p50":709.8419592500002,"p95":799.6956438578407,"p99":1402.9918393048895}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:43+00","p50":70.489897375,"p95":851.9272741994785,"p99":858.5057195615977}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:44+00","p50":391.36947599999996,"p95":759.578115629076,"p99":769.1995196102866}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:45+00","p50":589.8050069999999,"p95":742.1497717005353,"p99":1140.7987156319427}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:46+00","p50":513.372756,"p95":695.6635279966359,"p99":704.8919701683897}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:47+00","p50":83.28122362500001,"p95":735.7585928463415,"p99":1308.5254713411705}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:48+00","p50":685.88467175,"p95":736.3466384905232,"p99":1351.9836708891562}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:49+00","p50":368.31249275000005,"p95":796.3081886397514,"p99":804.4151197118568}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:50+00","p50":677.7274924999999,"p95":744.4511994089127,"p99":763.340511516162}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:51+00","p50":706.737086375,"p95":794.0885978485426,"p99":1408.1633451207372}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:52+00","p50":712.498727,"p95":835.2446833249347,"p99":1063.0755830233677}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:53+00","p50":736.810219375,"p95":847.5518238347482,"p99":1505.7651084142024}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:54+00","p50":98.80729975,"p95":853.3356684531628,"p99":885.5650648690918}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:55+00","p50":92.6406035,"p95":913.7277507810719,"p99":978.1990902109233}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:56+00","p50":927.88119675,"p95":1011.4514442814849,"p99":1105.8025877064438}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:57+00","p50":309.0496225,"p95":923.5739528932534,"p99":932.5648408622475}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:58+00","p50":743.4836947499999,"p95":820.4721675028458,"p99":880.8001259287948}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:52:59+00","p50":652.2214865000001,"p95":755.5188999842694,"p99":809.1808798646592}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:00+00","p50":126.53018275,"p95":713.594129300719,"p99":729.2276712761469}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:01+00","p50":638.62691175,"p95":728.1377819628505,"p99":1274.1109005824483}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:02+00","p50":691.744254,"p95":792.9605318433521,"p99":1431.235736979889}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:03+00","p50":537.1987875,"p95":802.1479546162304,"p99":1424.2666404140161}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:04+00","p50":473.81458849999996,"p95":786.1479706905264,"p99":1478.7761906006142}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:05+00","p50":258.051573125,"p95":831.978598839066,"p99":846.0585598225111}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:06+00","p50":456.20850899999994,"p95":924.0892146114836,"p99":1558.8452717433242}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:07+00","p50":314.71049825,"p95":933.8811926031929,"p99":1707.1655658590946}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:08+00","p50":775.409857,"p95":975.4365998819809,"p99":1656.1456925538996}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:09+00","p50":110.547124,"p95":1117.181946759245,"p99":1127.8060843717299}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:10+00","p50":767.620803,"p95":1042.0406080180117,"p99":1309.4963813683921}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:11+00","p50":607.9809836249999,"p95":915.7698622526977,"p99":1570.0191333897922}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:12+00","p50":89.562387125,"p95":878.3407359979426,"p99":899.1404894498996}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:13+00","p50":88.6619585,"p95":798.0805412489797,"p99":1405.3817943287004}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:14+00","p50":668.8410852500001,"p95":819.1141733492559,"p99":824.0586315541199}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:15+00","p50":696.505823,"p95":780.1112172863503,"p99":805.3501474909916}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:16+00","p50":91.98774425,"p95":769.3707800777007,"p99":1387.9460334639662}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:17+00","p50":71.8714895,"p95":823.7975466271273,"p99":830.4677052482333}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:18+00","p50":726.003165625,"p95":917.3226810546828,"p99":1610.7282485573123}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:19+00","p50":757.7068185,"p95":899.1141407886715,"p99":938.8606314053344}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:20+00","p50":93.664991875,"p95":960.3124896953208,"p99":1554.7662746333501}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:21+00","p50":80.3998105,"p95":1027.4531707635945,"p99":1051.2466368056319}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:22+00","p50":887.13558125,"p95":960.7300995614152,"p99":1866.1168454936214}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:23+00","p50":839.1753606249999,"p95":918.4161855278194,"p99":1705.9288193379975}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:24+00","p50":124.061086625,"p95":944.9606681098404,"p99":958.4953315487709}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:25+00","p50":94.89404174999999,"p95":921.1161727413044,"p99":1720.4540224854359}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:26+00","p50":97.720673,"p95":1126.424586184653,"p99":1919.2160464139135}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:27+00","p50":110.25215374999999,"p95":1097.7463671493879,"p99":2025.5486531077186}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:28+00","p50":994.4320807500001,"p95":1146.616052295021,"p99":2018.8332108960049}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:29+00","p50":496.26118775,"p95":1032.2819606357682,"p99":1082.4820833712092}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:30+00","p50":95.26289249999999,"p95":1063.2008627639586,"p99":1089.7234669510538}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:31+00","p50":322.123466125,"p95":1084.4904861774728,"p99":1096.390176416539}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:32+00","p50":946.771260125,"p95":1043.5533155349435,"p99":1062.3142757710948}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:33+00","p50":851.4897639999999,"p95":966.8122975787838,"p99":984.5058673300371}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:34+00","p50":605.8106183750001,"p95":899.4610023085004,"p99":935.6061972991525}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:35+00","p50":112.32648675,"p95":723.7360421248254,"p99":1345.0083593776073}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:36+00","p50":66.97210525,"p95":981.2340081082191,"p99":1573.6370532984977}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:37+00","p50":803.52137725,"p95":1005.8508072635784,"p99":1688.514803863432}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:38+00","p50":780.4130411250001,"p95":901.5892244313052,"p99":1464.971402960332}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:39+00","p50":104.35563575,"p95":888.3967168170545,"p99":1480.1829200265945}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:40+00","p50":116.935572875,"p95":766.3784737596985,"p99":1348.3354501088727}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:41+00","p50":94.58685775,"p95":783.3183632932814,"p99":1435.0542866992737}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:42+00","p50":74.37139024999999,"p95":832.141426173269,"p99":1387.697732650154}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:43+00","p50":112.60688425000001,"p95":952.3259757261523,"p99":999.1723450949239}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:44+00","p50":96.874685,"p95":903.9472246608348,"p99":925.6884169324741}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:45+00","p50":125.67214562500001,"p95":900.9926918734349,"p99":1635.2506788982778}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:46+00","p50":447.05422125,"p95":882.5993304561382,"p99":893.5271466432414}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:47+00","p50":198.78578125,"p95":881.8866655181238,"p99":901.7563913143451}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:48+00","p50":99.05300975,"p95":890.7551918429438,"p99":937.1381561441841}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:49+00","p50":102.78696475,"p95":989.895956138457,"p99":1613.9550990758103}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:50+00","p50":781.9674237500001,"p95":918.0380018531887,"p99":1717.697765880185}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:51+00","p50":738.4070765,"p95":934.4725887508049,"p99":1635.4098482267714}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:52+00","p50":125.3303855,"p95":904.6347778764547,"p99":921.5545171797404}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:53+00","p50":73.19908699999999,"p95":920.6957871640775,"p99":1025.64797573954}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:54+00","p50":492.5188125,"p95":1010.546223150423,"p99":1032.46183604953}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:55+00","p50":91.60272037499999,"p95":1014.3262149656085,"p99":1040.1120103450805}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:56+00","p50":115.77108175,"p95":1185.573371161823,"p99":1360.5931665024052}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:57+00","p50":603.1743595,"p95":1221.1205456577936,"p99":2197.0458986019553}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:58+00","p50":798.686587875,"p95":1166.9248242872156,"p99":1177.7436405427027}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:53:59+00","p50":38.438286999999995,"p95":684.5901946465209,"p99":732.1470027226043}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:54:00+00","p50":26.272826777777777,"p95":44.100337771842774,"p99":51.6662716908536}, + {"metric_name":"oidc_auth_request_finalize","timestamp":"2025-02-25 14:54:01+00","p50":30.017878,"p95":43.642520646738525,"p99":44.252627}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:23:59+00","p50":1030.5,"p95":1471.4999916553497,"p99":1521.4000186920166}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:00+00","p50":2008.0,"p95":2459.1999884843826,"p99":2523.320080280304}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:01+00","p50":2776.375,"p95":3373.6999965906143,"p99":3425.1400027275085}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:02+00","p50":3573.625,"p95":4345.099996447563,"p99":4404.150163412094}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:03+00","p50":4405.75,"p95":5268.499991416931,"p99":5410.740039825439}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:04+00","p50":3806.5,"p95":6085.599892139435,"p99":6402.5200119018555}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:05+00","p50":4211.625,"p95":6667.199835300446,"p99":7129.320035934448}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:06+00","p50":3874.0,"p95":4932.749992668629,"p99":6895.422165393829}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:07+00","p50":4079.75,"p95":6469.149787962437,"p99":8872.220103740692}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:08+00","p50":3855.0,"p95":5426.799870967865,"p99":6230.580416679382}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:09+00","p50":4506.5,"p95":5912.599918842316,"p99":7290.460385322571}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:10+00","p50":4177.75,"p95":6097.499261975288,"p99":6814.990001440048}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:11+00","p50":4100.125,"p95":5687.549865782261,"p99":7297.960047721863}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:12+00","p50":4172.0,"p95":5892.998918533325,"p99":7782.800015449524}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:13+00","p50":3922.75,"p95":4932.19998550415,"p99":5516.360263824463}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:14+00","p50":3931.625,"p95":5025.399993181229,"p99":6465.961674690247}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:15+00","p50":4126.5,"p95":5208.399711608887,"p99":5782.400260925293}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:16+00","p50":4052.0,"p95":4801.499925971031,"p99":5225.650760889053}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:17+00","p50":4097.75,"p95":4828.199954509735,"p99":5244.390195608139}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:18+00","p50":4175.0,"p95":4931.399954915047,"p99":5240.9202489852905}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:19+00","p50":4322.625,"p95":5171.699978470802,"p99":5579.3506581783295}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:20+00","p50":4379.125,"p95":5043.9499569535255,"p99":5381.400269508362}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:21+00","p50":4284.625,"p95":4718.549998342991,"p99":4893.400185585022}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:22+00","p50":4331.5,"p95":4954.699956297874,"p99":5118.540277004242}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:23+00","p50":4196.25,"p95":4875.899942040443,"p99":5322.74011182785}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:24+00","p50":4142.25,"p95":4740.399970412254,"p99":5309.080072402954}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:25+00","p50":4177.125,"p95":4768.44999474287,"p99":5276.780036449432}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:26+00","p50":4097.25,"p95":4826.599994778633,"p99":4900.080037593842}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:27+00","p50":4115.625,"p95":4690.299996495247,"p99":5154.210150003433}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:28+00","p50":4124.0,"p95":4587.49999833107,"p99":4650.000026702881}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:29+00","p50":4160.625,"p95":4723.4499787688255,"p99":5199.060081005096}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:30+00","p50":4166.875,"p95":4744.0,"p99":4809.6000118255615}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:31+00","p50":4103.375,"p95":4678.549963533878,"p99":4727.800030708313}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:32+00","p50":4049.25,"p95":4669.399996519089,"p99":4776.320045948029}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:33+00","p50":3930.5,"p95":4534.399989128113,"p99":4964.660097122192}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:34+00","p50":3873.625,"p95":4548.999992609024,"p99":4848.9504153728485}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:35+00","p50":3904.75,"p95":4509.249989926815,"p99":5025.110211133957}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:36+00","p50":3788.5,"p95":4529.999962806702,"p99":4969.6200342178345}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:37+00","p50":3797.5,"p95":4326.699987649918,"p99":4766.720333099365}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:38+00","p50":3875.5,"p95":4334.1999942064285,"p99":4441.9600648880005}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:39+00","p50":3803.0,"p95":4303.399992465973,"p99":4540.720024108887}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:40+00","p50":3755.0,"p95":4337.999959468842,"p99":4739.600141048431}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:41+00","p50":3797.0,"p95":4016.999990582466,"p99":4159.000075340271}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:42+00","p50":3695.0,"p95":4004.6999700069427,"p99":4127.5001764297485}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:43+00","p50":3921.25,"p95":4142.49999165535,"p99":4234.400218963623}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:44+00","p50":3935.125,"p95":4178.249990642071,"p99":4450.860002994537}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:45+00","p50":4021.5,"p95":4187.19998550415,"p99":4267.720020294189}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:46+00","p50":3904.75,"p95":4306.249998033047,"p99":4499.000220298767}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:47+00","p50":3834.5,"p95":4182.249994456768,"p99":4451.000029563904}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:48+00","p50":3889.25,"p95":4186.4999849796295,"p99":4360.400005340576}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:49+00","p50":3903.0,"p95":4059.749991238117,"p99":4141.8801345825195}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:50+00","p50":4043.25,"p95":4306.599994778633,"p99":4438.560820102692}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:51+00","p50":4157.875,"p95":4378.549994528294,"p99":4795.180574893951}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:52+00","p50":4091.5,"p95":4424.499981641769,"p99":4577.900305747986}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:53+00","p50":4165.125,"p95":4291.099996685982,"p99":4579.030361890793}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:54+00","p50":4164.875,"p95":4343.249990880489,"p99":4610.130042314529}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:55+00","p50":4082.5,"p95":4315.199993610382,"p99":4658.00003194809}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:56+00","p50":4219.0,"p95":4481.0,"p99":4525.400039672852}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:57+00","p50":4226.5,"p95":4496.39999628067,"p99":4552.640008926392}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:58+00","p50":4123.875,"p95":4362.49998819828,"p99":4683.650234460831}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:24:59+00","p50":4102.75,"p95":4255.499992132187,"p99":4361.600088119507}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:00+00","p50":4103.75,"p95":4313.049998223782,"p99":4417.260036945343}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:01+00","p50":4005.5,"p95":4157.4999849796295,"p99":4333.600448608398}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:02+00","p50":4061.0,"p95":4221.799984931946,"p99":4382.000075340271}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:03+00","p50":4066.875,"p95":4370.499945998192,"p99":4525.990001440048}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:04+00","p50":3940.25,"p95":4099.0,"p99":4245.240178108215}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:05+00","p50":3883.875,"p95":4061.0499791502953,"p99":4303.370162248611}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:06+00","p50":3853.625,"p95":4061.0,"p99":4140.920011520386}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:07+00","p50":3890.0,"p95":4247.099987983704,"p99":4466.60001373291}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:08+00","p50":4015.25,"p95":4204.999990701675,"p99":4289.800271511078}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:09+00","p50":4129.25,"p95":4358.599974632263,"p99":4596.3401927948}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:10+00","p50":4144.0,"p95":4413.699996113777,"p99":4515.920024871826}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:11+00","p50":4035.75,"p95":4221.999970436096,"p99":4523.650187730789}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:12+00","p50":3910.875,"p95":4125.799973487854,"p99":4353.86003446579}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:13+00","p50":3952.0,"p95":4237.349994957447,"p99":4530.26015329361}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:14+00","p50":3952.5,"p95":4271.949997961521,"p99":4581.670119047165}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:15+00","p50":3983.0,"p95":4324.499988913536,"p99":4381.200023651123}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:16+00","p50":3941.125,"p95":4371.199992895126,"p99":4528.5503623485565}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:17+00","p50":3849.0,"p95":4337.99996471405,"p99":4483.040285110474}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:18+00","p50":3956.0,"p95":4395.249924600124,"p99":4533.450051546097}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:19+00","p50":4224.5,"p95":4910.499987125397,"p99":5161.800029754639}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:20+00","p50":4441.875,"p95":5022.499989628792,"p99":5249.5503470897675}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:21+00","p50":4471.375,"p95":4955.549943745136,"p99":5154.040370941162}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:22+00","p50":4464.25,"p95":5003.9999660253525,"p99":5427.0006194114685}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:23+00","p50":4181.125,"p95":5119.749932587147,"p99":5405.0003871917725}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:24+00","p50":4047.875,"p95":4781.44999808073,"p99":4854.5000767707825}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:25+00","p50":3996.25,"p95":4810.299998283386,"p99":5201.880340576172}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:26+00","p50":4008.25,"p95":4664.799966931343,"p99":4766.360221385956}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:27+00","p50":4045.375,"p95":4860.699989199638,"p99":4918.640051841736}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:28+00","p50":4011.75,"p95":4731.249952018261,"p99":4865.850099802017}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:29+00","p50":4015.5,"p95":4821.649995028973,"p99":5320.800238609314}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:30+00","p50":3920.5,"p95":4867.9999532699585,"p99":4942.100001335144}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:31+00","p50":3921.5,"p95":4992.099996209145,"p99":5583.490210771561}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:32+00","p50":4307.875,"p95":4992.249991834164,"p99":5725.690670251846}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:33+00","p50":3996.5,"p95":5160.799988150597,"p99":5579.9601855278015}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:34+00","p50":4146.5,"p95":5195.199965119362,"p99":5695.080584526062}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:35+00","p50":4010.625,"p95":4989.299996256828,"p99":5073.130061388016}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:36+00","p50":4075.875,"p95":4986.3999812603,"p99":5132.090638399124}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:37+00","p50":4414.25,"p95":5255.399961948395,"p99":5797.680131912231}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:38+00","p50":4082.5,"p95":5291.49999165535,"p99":5399.300044059753}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:39+00","p50":4305.5,"p95":5327.099997997284,"p99":5932.1405239105225}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:40+00","p50":4035.5,"p95":5029.599964380264,"p99":5625.201036930084}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:41+00","p50":3918.75,"p95":5076.999992132187,"p99":5185.200081825256}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:42+00","p50":3652.0,"p95":4646.999967813492,"p99":5527.000163078308}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:43+00","p50":3791.25,"p95":4578.39999294281,"p99":4651.120008468628}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:44+00","p50":3994.625,"p95":4683.149998247623,"p99":4786.560914039612}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:45+00","p50":3984.625,"p95":4829.449984014034,"p99":5254.220173358917}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:46+00","p50":4010.75,"p95":4868.299998044968,"p99":4910.580004692078}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:47+00","p50":3885.625,"p95":4618.349998056889,"p99":4737.550101041794}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:48+00","p50":3949.25,"p95":4696.849994599819,"p99":4744.9300100803375}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:49+00","p50":3778.0,"p95":4783.599998354912,"p99":5476.320606708527}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:50+00","p50":3949.5,"p95":4780.2499913573265,"p99":4800.300008296967}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:51+00","p50":4073.0,"p95":4824.149950563908,"p99":5324.210004091263}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:52+00","p50":4369.25,"p95":5044.749929726124,"p99":5396.14000749588}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:53+00","p50":4330.5,"p95":5037.199989557266,"p99":5211.760583400726}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:54+00","p50":4276.625,"p95":5003.549998104572,"p99":5313.850356340408}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:55+00","p50":4087.0,"p95":4915.499961614609,"p99":5227.700289726257}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:56+00","p50":4214.0,"p95":5021.099974870682,"p99":5171.900372505188}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:57+00","p50":4206.75,"p95":4909.349994957447,"p99":5244.290108919144}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:58+00","p50":4299.75,"p95":4801.2499205470085,"p99":5073.000325202942}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:25:59+00","p50":4097.75,"p95":4671.599968671799,"p99":5133.1200041770935}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:00+00","p50":4268.625,"p95":4687.549998104572,"p99":4708.550007581711}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:01+00","p50":4048.5,"p95":4328.999969959259,"p99":4490.000520706177}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:02+00","p50":3924.5,"p95":4113.999980211258,"p99":4293.680003166199}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:03+00","p50":3904.25,"p95":4118.499990940094,"p99":4187.80001449585}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:04+00","p50":3965.125,"p95":4130.749980986118,"p99":4357.500124454498}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:05+00","p50":3906.5,"p95":4066.9999811649323,"p99":4263.080111503601}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:06+00","p50":3939.75,"p95":4102.799996376038,"p99":4327.000144958496}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:07+00","p50":3942.75,"p95":4171.899988770485,"p99":4486.39003443718}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:08+00","p50":3853.875,"p95":4122.099996209145,"p99":4362.9101531505585}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:09+00","p50":3849.75,"p95":4137.79998588562,"p99":4201.20001411438}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:10+00","p50":3876.75,"p95":4333.5999965667725,"p99":4756.100048065186}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:11+00","p50":4069.125,"p95":4391.199992418289,"p99":4757.510395765305}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:12+00","p50":3899.5,"p95":4318.749972879887,"p99":4636.50001001358}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:13+00","p50":3799.125,"p95":4383.749961197376,"p99":4449.75003695488}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:14+00","p50":3688.0,"p95":4368.0,"p99":4795.070597410202}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:15+00","p50":3736.75,"p95":4387.1999834775925,"p99":4466.8800411224365}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:16+00","p50":3820.0,"p95":4440.699979543686,"p99":4912.040618896484}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:17+00","p50":3846.25,"p95":4517.699998140335,"p99":4910.060519218445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:18+00","p50":3880.5,"p95":4714.199988484383,"p99":4987.320376396179}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:19+00","p50":4083.625,"p95":4919.049979388714,"p99":5437.6802587509155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:20+00","p50":4104.5,"p95":4940.0,"p99":5539.190038442612}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:21+00","p50":4337.125,"p95":5065.299978137016,"p99":5126.160079956055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:22+00","p50":4299.0,"p95":5138.399989128113,"p99":5472.580247879028}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:23+00","p50":4157.5,"p95":5173.299993991852,"p99":5657.800785064697}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:24+00","p50":4005.5,"p95":4916.899907588959,"p99":5245.360336303711}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:25+00","p50":3831.25,"p95":4771.199989557266,"p99":5240.000208854675}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:26+00","p50":3921.0,"p95":4608.699934720993,"p99":5159.160152435303}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:27+00","p50":3968.25,"p95":4675.799992322922,"p99":5110.410566568375}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:28+00","p50":3996.25,"p95":4773.549983799458,"p99":5205.69047665596}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:29+00","p50":4077.75,"p95":4755.799979805946,"p99":4808.960001468658}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:30+00","p50":4002.25,"p95":4718.999996423721,"p99":4819.000024318695}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:31+00","p50":3934.5,"p95":4697.999986171722,"p99":4784.400011062622}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:32+00","p50":3974.5,"p95":4760.749939024448,"p99":5119.200053215027}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:33+00","p50":3974.5,"p95":4728.999998211861,"p99":4823.0000286102295}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:34+00","p50":3930.5,"p95":4729.699973344803,"p99":4832.500041007996}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:35+00","p50":3779.0,"p95":4650.399986743927,"p99":5093.360100746155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:36+00","p50":3865.0,"p95":4670.54998165369,"p99":4823.360136985779}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:37+00","p50":3829.25,"p95":4575.249994456768,"p99":4643.450016260147}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:38+00","p50":3745.125,"p95":4647.499946713448,"p99":5150.19002699852}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:39+00","p50":3901.75,"p95":4685.199993491173,"p99":4746.360046863556}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:40+00","p50":3582.625,"p95":4458.149986565113,"p99":4514.780003070831}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:41+00","p50":3609.25,"p95":4516.999964952469,"p99":4983.940137386322}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:42+00","p50":3880.0,"p95":4573.799982428551,"p99":5050.080144405365}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:43+00","p50":3911.0,"p95":4579.999990344048,"p99":4641.52000617981}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:44+00","p50":3983.75,"p95":4585.249990403652,"p99":4681.060082912445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:45+00","p50":3916.0,"p95":4638.79999434948,"p99":4769.600120544434}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:46+00","p50":3850.75,"p95":4699.199974298477,"p99":5324.480092525482}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:47+00","p50":3766.75,"p95":4582.249964892864,"p99":4970.350048780441}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:48+00","p50":3894.25,"p95":4577.799968719482,"p99":4645.760025024414}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:49+00","p50":4014.5,"p95":4605.199998259544,"p99":4694.320080757141}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:50+00","p50":3913.75,"p95":4732.449975669384,"p99":5060.210444688797}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:51+00","p50":3997.625,"p95":4875.4999841451645,"p99":5056.510764837265}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:52+00","p50":3975.875,"p95":4810.199991464615,"p99":4964.610155344009}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:53+00","p50":3838.5,"p95":4584.999971032143,"p99":5131.800247192383}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:54+00","p50":3635.625,"p95":4575.499995827675,"p99":4972.250759363174}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:55+00","p50":3800.25,"p95":4669.699958324432,"p99":5190.440113067627}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:56+00","p50":3653.375,"p95":4491.399979829788,"p99":4548.440021514893}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:57+00","p50":3971.5,"p95":4599.1999616622925,"p99":4795.080368041992}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:58+00","p50":4150.0,"p95":4863.749983608723,"p99":5036.000581741333}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:26:59+00","p50":4330.5,"p95":4920.149979412556,"p99":5227.900344371796}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:00+00","p50":4187.5,"p95":4887.9999614953995,"p99":5284.000462055206}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:01+00","p50":3953.5,"p95":4573.9999804496765,"p99":4676.860001564026}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:02+00","p50":3720.25,"p95":4681.099977135658,"p99":4890.570508241653}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:03+00","p50":3820.375,"p95":4533.499980568886,"p99":4674.120118141174}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:04+00","p50":3837.5,"p95":4342.899931192398,"p99":4507.940075874329}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:05+00","p50":4100.5,"p95":4748.299998521805,"p99":4848.5600662231445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:06+00","p50":4092.75,"p95":4892.349985897541,"p99":5255.830230474472}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:07+00","p50":3963.0,"p95":4739.249991595745,"p99":4824.620831012726}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:08+00","p50":4019.375,"p95":4945.699976801872,"p99":5495.920095443726}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:09+00","p50":4053.625,"p95":5160.9499569535255,"p99":5551.210294961929}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:10+00","p50":3875.25,"p95":4948.599933862686,"p99":5048.0400013923645}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:11+00","p50":4221.75,"p95":4929.44998639822,"p99":5432.540065288544}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:12+00","p50":3804.5,"p95":5025.39998626709,"p99":5135.500858306885}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:13+00","p50":3948.5,"p95":5096.999980926514,"p99":5133.600021362305}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:14+00","p50":3896.5,"p95":4878.099957942963,"p99":5511.3400592803955}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:15+00","p50":3855.75,"p95":4704.0,"p99":5297.800350189209}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:16+00","p50":3573.25,"p95":4849.999996423721,"p99":5033.001261711121}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:17+00","p50":3677.25,"p95":4775.299995779991,"p99":4870.22002363205}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:18+00","p50":4024.5,"p95":4744.599940896034,"p99":5297.000095844269}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:19+00","p50":4111.5,"p95":4898.499931573868,"p99":5356.800024032593}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:20+00","p50":3971.375,"p95":5104.9499297738075,"p99":5272.48051738739}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:21+00","p50":4170.375,"p95":5130.249973714352,"p99":5237.990466833115}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:22+00","p50":4229.625,"p95":5195.499989628792,"p99":5855.500124454498}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:23+00","p50":4177.0,"p95":5339.899960637093,"p99":5560.950442075729}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:24+00","p50":3996.5,"p95":5533.799990415573,"p99":6033.240209579468}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:25+00","p50":4319.125,"p95":5627.499983906746,"p99":6490.450390100479}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:26+00","p50":4661.125,"p95":5708.399993181229,"p99":6435.23012137413}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:27+00","p50":4691.0,"p95":5637.649998366833,"p99":5843.880883216858}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:28+00","p50":4026.5,"p95":5861.999992489815,"p99":5980.600018024445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:29+00","p50":4617.75,"p95":5865.749991714954,"p99":6794.560127258301}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:30+00","p50":4725.5,"p95":5978.0,"p99":6034.120108604431}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:31+00","p50":4103.75,"p95":5863.599993228912,"p99":6663.680265426636}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:32+00","p50":4642.25,"p95":5840.24993366003,"p99":5913.210047006607}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:33+00","p50":3652.625,"p95":5668.999992132187,"p99":6459.801271438599}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:34+00","p50":4262.125,"p95":5368.1999933719635,"p99":6331.110266447067}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:35+00","p50":3527.5,"p95":5311.249969422817,"p99":6153.860055446625}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:36+00","p50":3934.0,"p95":5173.599989414215,"p99":5891.180153846741}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:37+00","p50":4116.5,"p95":5068.799966096878,"p99":5242.440198898315}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:38+00","p50":4031.0,"p95":5230.499969959259,"p99":5914.4001121521}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:39+00","p50":3418.0,"p95":4989.649994313717,"p99":5284.560175895691}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:40+00","p50":3374.0,"p95":5081.999980926514,"p99":5677.500816345215}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:41+00","p50":4183.0,"p95":5100.49998831749,"p99":5157.500060081482}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:42+00","p50":4258.0,"p95":5308.649972140789,"p99":6024.800046920776}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:43+00","p50":3950.375,"p95":5389.149988234043,"p99":6143.960193634033}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:44+00","p50":4169.75,"p95":5394.999990820885,"p99":6079.920994281769}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:45+00","p50":4266.25,"p95":5512.099962949753,"p99":6110.18029499054}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:46+00","p50":4544.5,"p95":5622.5999792814255,"p99":6291.400067806244}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:47+00","p50":3627.5,"p95":5456.199988484383,"p99":5598.720797538757}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:48+00","p50":4104.0,"p95":5441.899994850159,"p99":6312.460124969482}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:49+00","p50":4140.0,"p95":5368.999998211861,"p99":5420.0000858306885}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:50+00","p50":4036.5,"p95":5406.499990224838,"p99":6026.12092590332}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:51+00","p50":4036.75,"p95":5048.699996590614,"p99":5221.750579595566}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:52+00","p50":4218.625,"p95":5293.0499722361565,"p99":6023.950803518295}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:53+00","p50":4084.5,"p95":5266.199996232986,"p99":5986.801077365875}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:54+00","p50":4127.0,"p95":5208.199995994568,"p99":5949.380254745483}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:55+00","p50":4150.25,"p95":5058.899996876717,"p99":5794.910111188889}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:56+00","p50":3715.0,"p95":5351.749995529652,"p99":5390.500002384186}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:57+00","p50":4383.125,"p95":5603.149998247623,"p99":6327.390018224716}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:58+00","p50":4050.0,"p95":5555.399987220764,"p99":5772.72088432312}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:27:59+00","p50":4687.875,"p95":5634.299996256828,"p99":5755.29007935524}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:00+00","p50":4459.0,"p95":5667.39999628067,"p99":5808.98009967804}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:01+00","p50":4315.75,"p95":5632.49997985363,"p99":6221.5809960365295}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:02+00","p50":3458.75,"p95":5284.0,"p99":5331.040252685547}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:03+00","p50":3855.0,"p95":5323.999987483025,"p99":5434.000998497009}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:04+00","p50":4278.75,"p95":5179.1999888420105,"p99":5322.600133895874}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:05+00","p50":3616.0,"p95":5140.29999601841,"p99":5747.000796318054}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:06+00","p50":3969.0,"p95":4978.999980688095,"p99":5096.280086517334}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:07+00","p50":3567.75,"p95":5036.99998664856,"p99":5826.700222969055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:08+00","p50":4092.5,"p95":5039.999981880188,"p99":5080.660024642944}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:09+00","p50":3891.25,"p95":4845.799979805946,"p99":5407.800741672516}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:10+00","p50":3904.375,"p95":4896.199976682663,"p99":5342.090477228165}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:11+00","p50":3644.0,"p95":4846.999996185303,"p99":4931.4000244140625}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:12+00","p50":3989.375,"p95":4795.049968183041,"p99":4889.110040426254}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:13+00","p50":3835.375,"p95":4722.8999761343,"p99":5127.500613689423}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:14+00","p50":3678.75,"p95":4616.099967598915,"p99":4950.770177125931}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:15+00","p50":3924.0,"p95":4508.499979019165,"p99":4885.8001708984375}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:16+00","p50":3992.25,"p95":4278.999976396561,"p99":4397.150092840195}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:17+00","p50":3786.375,"p95":4170.999992609024,"p99":4440.500014781952}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:18+00","p50":3912.25,"p95":4166.699995279312,"p99":4493.180190086365}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:19+00","p50":4000.5,"p95":4168.699998378754,"p99":4351.36003112793}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:20+00","p50":4084.75,"p95":4237.499972820282,"p99":4439.300050735474}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:21+00","p50":4211.5,"p95":4533.29996919632,"p99":4873.2200565338135}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:22+00","p50":4098.5,"p95":4438.399940133095,"p99":4808.520160675049}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:23+00","p50":3888.5,"p95":4279.899996638298,"p99":4642.25003361702}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:24+00","p50":4064.875,"p95":4308.749994814396,"p99":4538.750076055527}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:25+00","p50":3920.0,"p95":4333.49998664856,"p99":4422.800048828125}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:26+00","p50":3969.375,"p95":4330.44999474287,"p99":4369.690032243729}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:27+00","p50":4114.75,"p95":4614.999998450279,"p99":4728.000105381012}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:28+00","p50":4067.625,"p95":4761.749932587147,"p99":4847.200033187866}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:29+00","p50":4153.875,"p95":4607.0,"p99":4757.630115270615}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:30+00","p50":4195.5,"p95":4718.949985086918,"p99":4817.880408287048}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:31+00","p50":4163.25,"p95":4675.749994814396,"p99":4851.800409317017}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:32+00","p50":4035.5,"p95":4701.2999811172485,"p99":5156.840019226074}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:33+00","p50":4074.75,"p95":4620.249990403652,"p99":4944.590354681015}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:34+00","p50":4027.5,"p95":4585.0999982357025,"p99":4653.400239944458}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:35+00","p50":3965.0,"p95":4462.399991989136,"p99":4503.460004806519}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:36+00","p50":3857.0,"p95":4189.299983263016,"p99":4489.760377883911}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:37+00","p50":3783.75,"p95":4159.599994778633,"p99":4650.640057086945}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:38+00","p50":3824.0,"p95":4254.099994421005,"p99":4360.7200565338135}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:39+00","p50":3905.25,"p95":4185.499991178513,"p99":4276.120079040527}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:40+00","p50":3886.125,"p95":4211.0999883413315,"p99":4399.700015544891}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:41+00","p50":3871.75,"p95":4216.0,"p99":4292.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:42+00","p50":3873.0,"p95":4166.699967384338,"p99":4396.920112609863}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:43+00","p50":3849.25,"p95":4157.0,"p99":4378.760008811951}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:44+00","p50":3954.0,"p95":4297.999924898148,"p99":4510.000067234039}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:45+00","p50":4019.5,"p95":4201.349982082844,"p99":4411.000159263611}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:46+00","p50":3885.375,"p95":4108.249991118908,"p99":4366.480068206787}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:47+00","p50":3898.75,"p95":4311.749980986118,"p99":4512.650239229202}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:48+00","p50":4120.0,"p95":4340.599996805191,"p99":4660.320002555847}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:49+00","p50":4124.625,"p95":4286.249987065792,"p99":4482.500280857086}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:50+00","p50":4113.75,"p95":4336.04982060194,"p99":4569.160164833069}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:51+00","p50":4215.375,"p95":4420.449998319149,"p99":4500.670488119125}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:52+00","p50":4046.375,"p95":4468.499974131584,"p99":4928.150527715683}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:53+00","p50":3998.0,"p95":4281.749991238117,"p99":4342.260058879852}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:54+00","p50":4084.25,"p95":4411.399970412254,"p99":4575.400048732758}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:55+00","p50":3991.375,"p95":4236.3999927043915,"p99":4368.940148830414}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:56+00","p50":4071.0,"p95":4320.949995100498,"p99":4576.900300502777}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:57+00","p50":4208.25,"p95":4446.0999969244,"p99":4502.100012302399}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:58+00","p50":4230.125,"p95":4485.149963200092,"p99":4767.690032243729}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:28:59+00","p50":4218.0,"p95":4468.999985694885,"p99":4616.0001974105835}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:00+00","p50":4188.5,"p95":4370.0,"p99":4510.95002245903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:01+00","p50":4019.375,"p95":4184.9499943852425,"p99":4356.480053901672}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:02+00","p50":3895.0,"p95":4153.199996232986,"p99":4244.0400557518005}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:03+00","p50":3847.0,"p95":4045.19997215271,"p99":4212.400048732758}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:04+00","p50":3855.25,"p95":4045.199992418289,"p99":4160.010016679764}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:05+00","p50":3852.0,"p95":4148.399983048439,"p99":4379.0401310920715}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:06+00","p50":3823.75,"p95":4050.4999721050262,"p99":4188.940075874329}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:07+00","p50":3811.75,"p95":4007.499940276146,"p99":4105.750039815903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:08+00","p50":3802.5,"p95":4100.999973893166,"p99":4166.400048732758}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:09+00","p50":3828.5,"p95":4109.199978113174,"p99":4544.340469837189}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:10+00","p50":3824.25,"p95":4133.799968719482,"p99":4368.260142326355}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:11+00","p50":3821.75,"p95":4172.349994242191,"p99":4478.8904621601105}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:12+00","p50":3703.25,"p95":4124.2999856472015,"p99":4485.980067253113}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:13+00","p50":3653.125,"p95":4153.5999801158905,"p99":4232.970300912857}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:14+00","p50":3823.75,"p95":4347.499959349632,"p99":4469.500014781952}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:15+00","p50":3864.625,"p95":4464.499966740608,"p99":4698.450016260147}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:16+00","p50":3911.5,"p95":4392.149982511997,"p99":4708.920335769653}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:17+00","p50":3826.0,"p95":4473.399987697601,"p99":4526.120006561279}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:18+00","p50":3737.5,"p95":4381.599998116493,"p99":4529.080149173737}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:19+00","p50":3720.75,"p95":4373.999963760376,"p99":4458.88000869751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:20+00","p50":3724.25,"p95":4400.8999963998795,"p99":4835.020573139191}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:21+00","p50":3838.5,"p95":4427.999960422516,"p99":4500.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:22+00","p50":3817.5,"p95":4343.999978542328,"p99":4414.000118732452}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:23+00","p50":3999.25,"p95":4412.999998211861,"p99":4591.000220298767}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:24+00","p50":3822.875,"p95":4386.849998176098,"p99":4574.610164880753}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:25+00","p50":4057.125,"p95":4309.599985599518,"p99":4542.250108003616}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:26+00","p50":4068.125,"p95":4384.849988400936,"p99":4581.360100746155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:27+00","p50":4056.25,"p95":4189.999992847443,"p99":4260.000150203705}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:28+00","p50":4160.0,"p95":4308.649995028973,"p99":4476.9500596523285}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:29+00","p50":4137.0,"p95":4307.099947452545,"p99":4457.88000869751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:30+00","p50":4057.5,"p95":4231.0,"p99":4512.000095844269}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:31+00","p50":4071.0,"p95":4309.499982714653,"p99":4356.000248908997}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:32+00","p50":3980.625,"p95":4194.249998033047,"p99":4382.700160503387}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:33+00","p50":4070.5,"p95":4280.999992251396,"p99":4358.400058269501}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:34+00","p50":4106.25,"p95":4324.9999767541885,"p99":4564.000157356262}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:35+00","p50":4047.0,"p95":4248.249991118908,"p99":4420.440062522888}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:36+00","p50":4080.375,"p95":4389.799992799759,"p99":4493.910012960434}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:37+00","p50":4047.375,"p95":4375.999993085861,"p99":4480.850106477737}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:38+00","p50":4073.0,"p95":4478.399996995926,"p99":4619.480302810669}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:39+00","p50":4132.0,"p95":4690.599972844124,"p99":5013.680380821228}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:40+00","p50":4209.75,"p95":4744.249989688396,"p99":5183.720059394836}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:41+00","p50":4120.0,"p95":4605.499996304512,"p99":4664.450016260147}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:42+00","p50":4025.75,"p95":4735.499972820282,"p99":5284.880153656006}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:43+00","p50":3887.375,"p95":4620.799993753433,"p99":5290.500062465668}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:44+00","p50":4004.125,"p95":4735.399976968765,"p99":4783.580033779144}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:45+00","p50":3968.25,"p95":4633.0,"p99":4970.320442199707}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:46+00","p50":3955.25,"p95":4741.849988400936,"p99":5288.180845737457}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:47+00","p50":3936.0,"p95":4894.799966096878,"p99":4979.880058765411}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:48+00","p50":3950.375,"p95":4843.149978458881,"p99":4932.640031814575}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:49+00","p50":4025.0,"p95":4736.799998164177,"p99":5217.760669708252}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:50+00","p50":4035.5,"p95":4780.4999742507935,"p99":4923.041488647461}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:51+00","p50":4073.0,"p95":4818.199955940247,"p99":5348.460245132446}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:52+00","p50":3961.75,"p95":4795.499990701675,"p99":4912.260043144226}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:53+00","p50":3792.625,"p95":4817.99996137619,"p99":5085.100430011749}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:54+00","p50":3977.5,"p95":4887.599956989288,"p99":5334.340029716492}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:55+00","p50":3917.25,"p95":4612.5999965667725,"p99":4765.980457305908}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:56+00","p50":4103.0,"p95":4783.799989700317,"p99":4847.360008239746}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:57+00","p50":4183.5,"p95":4852.499974131584,"p99":5344.500014781952}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:58+00","p50":4137.0,"p95":4870.999992966652,"p99":5470.360082149506}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:29:59+00","p50":4174.75,"p95":4825.599996089935,"p99":4940.300086021423}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:00+00","p50":4106.25,"p95":4907.5999965667725,"p99":5303.640266418457}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:01+00","p50":4097.5,"p95":4742.699989199638,"p99":5055.050136804581}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:02+00","p50":3952.5,"p95":4869.899980068207,"p99":5050.340120315552}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:03+00","p50":3949.0,"p95":4716.0,"p99":4905.760117530823}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:04+00","p50":3961.875,"p95":4656.199978113174,"p99":5090.680064201355}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:05+00","p50":3904.0,"p95":4588.599987149239,"p99":5198.840079307556}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:06+00","p50":3889.0,"p95":4515.399964213371,"p99":4579.320031642914}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:07+00","p50":3863.625,"p95":4421.399978399277,"p99":4485.860020160675}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:08+00","p50":3874.75,"p95":4472.49999833107,"p99":4531.700009346008}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:09+00","p50":3929.5,"p95":4444.9999858140945,"p99":4468.800001621246}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:10+00","p50":3931.375,"p95":4326.0,"p99":4641.110051870346}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:11+00","p50":3971.25,"p95":4324.199973106384,"p99":4396.090001344681}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:12+00","p50":3928.125,"p95":4286.099995970726,"p99":4353.100016117096}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:13+00","p50":3859.5,"p95":4135.699953198433,"p99":4338.630197286606}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:14+00","p50":3998.25,"p95":4257.099989771843,"p99":4482.350143194199}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:15+00","p50":3943.75,"p95":4217.799961447716,"p99":4384.160140991211}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:16+00","p50":3885.75,"p95":4040.5999925136566,"p99":4214.95002245903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:17+00","p50":3971.75,"p95":4148.999982118607,"p99":4327.000020027161}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:18+00","p50":4003.375,"p95":4183.0,"p99":4424.470102071762}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:19+00","p50":4041.25,"p95":4205.0,"p99":4309.220050811768}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:20+00","p50":4082.5,"p95":4298.299987316132,"p99":4476.180059432983}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:21+00","p50":4216.5,"p95":4434.999990701675,"p99":4521.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:22+00","p50":4202.375,"p95":4487.249998271465,"p99":4799.0000829696655}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:23+00","p50":4192.25,"p95":4436.299998283386,"p99":4737.960159301758}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:24+00","p50":4200.5,"p95":4474.999979257584,"p99":4643.000110626221}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:25+00","p50":4178.5,"p95":4399.099962949753,"p99":4608.40002822876}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:26+00","p50":4148.625,"p95":4325.0,"p99":4519.350020170212}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:27+00","p50":4088.625,"p95":4286.549994528294,"p99":4369.610018968582}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:28+00","p50":4088.5,"p95":4235.699996590614,"p99":4420.870055913925}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:29+00","p50":4064.5,"p95":4269.49998664856,"p99":4461.700019836426}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:30+00","p50":3982.75,"p95":4208.499990701675,"p99":4280.160020828247}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:31+00","p50":3982.75,"p95":4155.49999165535,"p99":4279.300110816956}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:32+00","p50":3972.875,"p95":4160.549994528294,"p99":4394.040192604065}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:33+00","p50":3940.375,"p95":4253.799971580505,"p99":4424.090012788773}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:34+00","p50":3974.875,"p95":4348.499980092049,"p99":4463.570125818253}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:35+00","p50":3934.5,"p95":4185.799996614456,"p99":4293.6400446891785}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:36+00","p50":3900.625,"p95":4178.149986565113,"p99":4283.390078306198}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:37+00","p50":3877.75,"p95":4218.599962234497,"p99":4462.440170288086}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:38+00","p50":4043.75,"p95":4300.799978256226,"p99":4676.280414581299}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:39+00","p50":3894.0,"p95":4277.0,"p99":4581.440048217773}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:40+00","p50":3884.125,"p95":4247.699988484383,"p99":4335.670004606247}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:41+00","p50":3808.625,"p95":4272.999955654144,"p99":4451.250169992447}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:42+00","p50":3876.125,"p95":4147.149979412556,"p99":4380.860302448273}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:43+00","p50":3870.0,"p95":4108.599980831146,"p99":4418.320002555847}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:44+00","p50":3940.0,"p95":4220.849962174892,"p99":4498.97009396553}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:45+00","p50":3918.0,"p95":4238.499980568886,"p99":4326.970048189163}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:46+00","p50":3814.5,"p95":4070.9999918937683,"p99":4315.600019454956}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:47+00","p50":3741.0,"p95":4018.7999798059464,"p99":4118.360023498535}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:48+00","p50":3593.0,"p95":4075.1999942064285,"p99":4135.680016994476}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:49+00","p50":3794.0,"p95":4124.999996185303,"p99":4215.70011138916}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:50+00","p50":3747.5,"p95":4115.899981975555,"p99":4475.560173034668}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:51+00","p50":3706.0,"p95":4156.999982595444,"p99":4273.800515174866}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:52+00","p50":3869.0,"p95":4477.2999967336655,"p99":4651.890330553055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:53+00","p50":3958.375,"p95":4527.099989056587,"p99":4587.430027723312}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:54+00","p50":4036.875,"p95":4557.949987232685,"p99":4877.880005836487}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:55+00","p50":4074.25,"p95":4425.999982118607,"p99":4603.00012588501}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:56+00","p50":3955.0,"p95":4462.299996972084,"p99":4513.070010900497}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:57+00","p50":4260.5,"p95":4544.3999898433685,"p99":4606.920574188232}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:58+00","p50":4136.75,"p95":4575.0,"p99":4687.000137329102}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:30:59+00","p50":4224.5,"p95":4512.3999791145325,"p99":4708.5603675842285}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:00+00","p50":4165.5,"p95":4454.399949789047,"p99":4780.800324440002}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:01+00","p50":4016.0,"p95":4185.99998664856,"p99":4332.200283050537}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:02+00","p50":3968.625,"p95":4197.49994456768,"p99":4570.000118255615}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:03+00","p50":3872.5,"p95":4105.1999834775925,"p99":4357.400205612183}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:04+00","p50":3868.0,"p95":4061.3999762535095,"p99":4259.760101318359}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:05+00","p50":3759.5,"p95":4000.199995994568,"p99":4062.8600368499756}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:06+00","p50":3650.5,"p95":3952.599998116493,"p99":4144.320219993591}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:07+00","p50":3851.5,"p95":4118.799937844276,"p99":4439.880021095276}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:08+00","p50":3788.75,"p95":3991.449998319149,"p99":4085.6103079319}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:09+00","p50":3872.25,"p95":4100.249971568584,"p99":4237.67005610466}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:10+00","p50":3829.0,"p95":4309.549983799458,"p99":4383.660048961639}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:11+00","p50":3917.0,"p95":4278.199988126755,"p99":4607.200284957886}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:12+00","p50":3948.0,"p95":4280.599988818169,"p99":4481.120008945465}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:13+00","p50":3947.375,"p95":4160.9499943852425,"p99":4417.090019464493}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:14+00","p50":3944.625,"p95":4196.249994456768,"p99":4522.050028085709}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:15+00","p50":4120.0,"p95":4340.899998426437,"p99":4536.580039024353}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:16+00","p50":3947.25,"p95":4361.749926149845,"p99":4454.7600202560425}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:17+00","p50":3827.0,"p95":4169.799984335899,"p99":4493.760200500488}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:18+00","p50":4009.625,"p95":4381.649989068508,"p99":4576.830071210861}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:19+00","p50":4145.875,"p95":4400.399986743927,"p99":4610.010120630264}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:20+00","p50":4062.0,"p95":4448.399961709976,"p99":4772.720164299011}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:21+00","p50":4260.125,"p95":4697.89997446537,"p99":4918.440075874329}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:22+00","p50":4129.5,"p95":4715.799978971481,"p99":4812.010514497757}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:23+00","p50":4073.5,"p95":4626.599996328354,"p99":4990.680378913879}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:24+00","p50":3938.625,"p95":4515.799951076508,"p99":4640.020061969757}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:25+00","p50":3960.375,"p95":4377.399993181229,"p99":4578.700286388397}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:26+00","p50":3898.125,"p95":4455.799973487854,"p99":4711.2502319812775}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:27+00","p50":3996.5,"p95":4448.39999294281,"p99":4852.080570220947}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:28+00","p50":4154.75,"p95":4422.849978148937,"p99":4646.360005378723}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:29+00","p50":4087.0,"p95":4450.999942779541,"p99":4538.3000411987305}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:30+00","p50":3999.0,"p95":4275.999996185303,"p99":4446.10001373291}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:31+00","p50":3869.5,"p95":4140.199892640114,"p99":4570.080186843872}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:32+00","p50":3763.0,"p95":4027.099962949753,"p99":4248.460103034973}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:33+00","p50":3797.75,"p95":4158.599996089935,"p99":4357.600015640259}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:34+00","p50":3944.0,"p95":4232.399951934814,"p99":4490.100048065186}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:35+00","p50":3881.0,"p95":4183.9999878406525,"p99":4222.400004863739}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:36+00","p50":3751.875,"p95":4254.699996113777,"p99":4579.400031089783}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:37+00","p50":3740.5,"p95":4252.3999980688095,"p99":4615.680094242096}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:38+00","p50":3790.375,"p95":4328.949995100498,"p99":4759.520135879517}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:39+00","p50":3852.5,"p95":4314.499975800514,"p99":4431.800492286682}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:40+00","p50":3985.0,"p95":4447.599992275238,"p99":4867.600069522858}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:41+00","p50":3916.875,"p95":4365.599985599518,"p99":4665.940152645111}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:42+00","p50":3999.75,"p95":4416.199990272522,"p99":4794.060037612915}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:43+00","p50":4117.75,"p95":4542.599976301193,"p99":4642.56024646759}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:44+00","p50":4047.875,"p95":4389.699989199638,"p99":4681.99014544487}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:45+00","p50":4092.0,"p95":4385.999997973442,"p99":4504.800001621246}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:46+00","p50":4006.25,"p95":4182.599993228912,"p99":4299.12012052536}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:47+00","p50":3900.5,"p95":4023.0,"p99":4165.880132675171}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:48+00","p50":3894.375,"p95":4151.749998152256,"p99":4371.550249814987}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:49+00","p50":3882.0,"p95":4162.199977397919,"p99":4280.5200843811035}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:50+00","p50":3913.0,"p95":4207.799986481667,"p99":4447.160203933716}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:51+00","p50":3969.125,"p95":4172.999930858612,"p99":4415.80021572113}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:52+00","p50":3892.5,"p95":4356.599980831146,"p99":4572.040088176727}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:53+00","p50":3978.5,"p95":4367.199996471405,"p99":4435.3003034591675}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:54+00","p50":4041.0,"p95":4450.199939727783,"p99":4810.080149173737}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:55+00","p50":4015.75,"p95":4471.999996423721,"p99":4843.000158786774}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:56+00","p50":4078.5,"p95":4539.199996948242,"p99":4647.800231933594}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:57+00","p50":4154.375,"p95":4515.099996685982,"p99":4615.960180282593}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:58+00","p50":4209.5,"p95":4546.49998152256,"p99":4586.700008869171}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:31:59+00","p50":4322.0,"p95":4783.749993622303,"p99":4898.550086736679}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:00+00","p50":4472.0,"p95":4859.099983692169,"p99":5010.800086975098}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:01+00","p50":4285.25,"p95":4562.999981880188,"p99":4658.84001159668}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:02+00","p50":4261.75,"p95":4553.299951076508,"p99":4857.160423278809}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:03+00","p50":4065.875,"p95":4481.799992799759,"p99":4768.070133924484}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:04+00","p50":3939.0,"p95":4239.249994456768,"p99":4338.400047302246}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:05+00","p50":3858.375,"p95":4183.9999833106995,"p99":4497.250031709671}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:06+00","p50":3795.25,"p95":4088.0999962091446,"p99":4146.1900136470795}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:07+00","p50":3739.5,"p95":3964.7999489307404,"p99":4136.490024805069}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:08+00","p50":3748.75,"p95":3959.1499946713448,"p99":4333.970279932022}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:09+00","p50":3857.375,"p95":4077.499996304512,"p99":4203.000029563904}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:10+00","p50":3817.625,"p95":4024.399975538254,"p99":4233.190262556076}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:11+00","p50":3815.625,"p95":4012.3499765992165,"p99":4165.970004320145}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:12+00","p50":3767.0,"p95":3917.1999942064285,"p99":4108.760003089905}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:13+00","p50":3771.75,"p95":3987.199974298477,"p99":4161.120179176331}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:14+00","p50":3833.5,"p95":4050.1999850273132,"p99":4217.500074863434}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:15+00","p50":3843.125,"p95":4031.449959695339,"p99":4117.780003070831}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:16+00","p50":3810.5,"p95":3970.2999962568283,"p99":4093.830046415329}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:17+00","p50":3797.0,"p95":4035.999992609024,"p99":4272.850063562393}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:18+00","p50":3831.25,"p95":3984.549986898899,"p99":4328.100104808807}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:19+00","p50":3856.0,"p95":4037.699998140335,"p99":4093.5000371932983}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:20+00","p50":3810.375,"p95":3985.349998295307,"p99":4098.9302713871}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:21+00","p50":3937.25,"p95":4114.49999833107,"p99":4309.200082778931}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:22+00","p50":4019.5,"p95":4216.599998116493,"p99":4345.880058765411}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:23+00","p50":3986.0,"p95":4140.699994564056,"p99":4390.360118865967}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:24+00","p50":4078.25,"p95":4316.099996447563,"p99":4411.140161991119}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:25+00","p50":3959.25,"p95":4136.999983906746,"p99":4226.000051498413}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:26+00","p50":3984.5,"p95":4296.499968290329,"p99":4697.30024433136}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:27+00","p50":3997.0,"p95":4381.799987316132,"p99":4759.100038051605}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:28+00","p50":3977.625,"p95":4486.0,"p99":4725.130026102066}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:29+00","p50":4063.625,"p95":4496.099973797798,"p99":4800.460032939911}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:30+00","p50":4010.75,"p95":4552.249991118908,"p99":4608.291035890579}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:31+00","p50":3868.25,"p95":4568.24997228384,"p99":5142.450666666031}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:32+00","p50":3853.0,"p95":4449.799992084503,"p99":5027.120384693146}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:33+00","p50":3803.25,"p95":4516.599986076355,"p99":4904.880761623383}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:34+00","p50":3850.875,"p95":4621.749983370304,"p99":5088.150557279587}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:35+00","p50":3786.25,"p95":4590.2999856472015,"p99":5244.5801820755005}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:36+00","p50":3683.875,"p95":4476.049994170666,"p99":4940.9706699848175}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:37+00","p50":3578.0,"p95":4551.499975204468,"p99":4639.700065612793}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:38+00","p50":3837.5,"p95":4585.5999965667725,"p99":5147.480079650879}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:39+00","p50":3807.625,"p95":4490.499996304512,"p99":4570.200053215027}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:40+00","p50":3739.5,"p95":4591.599982619286,"p99":4915.920477390289}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:41+00","p50":3863.25,"p95":4512.599983692169,"p99":4632.340075016022}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:42+00","p50":3871.0,"p95":4752.899966001511,"p99":4979.240087509155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:43+00","p50":3932.75,"p95":4665.0,"p99":4753.960704803467}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:44+00","p50":4023.5,"p95":4660.399992465973,"p99":4842.840078353882}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:45+00","p50":4050.5,"p95":4665.999987483025,"p99":4985.000133037567}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:46+00","p50":3990.5,"p95":4819.599988698959,"p99":5189.760494232178}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:47+00","p50":3793.25,"p95":4637.8999979496,"p99":4711.380034446716}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:48+00","p50":3756.625,"p95":4493.549960196018,"p99":5083.560024261475}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:49+00","p50":3733.875,"p95":4509.649963080883,"p99":4903.740624904633}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:50+00","p50":3868.875,"p95":4518.649998128414,"p99":4572.930001497269}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:51+00","p50":3690.625,"p95":4580.549986898899,"p99":4665.970043420792}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:52+00","p50":3762.5,"p95":4587.499975204468,"p99":5026.800704956055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:53+00","p50":3781.5,"p95":4480.199955940247,"p99":4745.16010427475}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:54+00","p50":3748.0,"p95":4490.399988412857,"p99":4590.560018539429}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:55+00","p50":3807.25,"p95":4585.199994921684,"p99":5090.920100212097}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:56+00","p50":3870.375,"p95":4593.0,"p99":5095.3307201862335}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:57+00","p50":3772.125,"p95":4730.0,"p99":5036.400259971619}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:58+00","p50":4082.75,"p95":4733.799915552139,"p99":5342.880004405975}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:32:59+00","p50":4052.5,"p95":4815.249942719936,"p99":5192.100144863129}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:00+00","p50":4022.5,"p95":4759.799993515015,"p99":5288.980073928833}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:01+00","p50":4005.5,"p95":4787.4999796152115,"p99":5245.360136985779}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:02+00","p50":3789.5,"p95":4456.199955940247,"p99":4593.18007850647}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:03+00","p50":3864.375,"p95":4423.249991118908,"p99":4661.920272827148}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:04+00","p50":3746.875,"p95":4380.899996161461,"p99":4494.040055274963}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:05+00","p50":3830.0,"p95":4140.1999888420105,"p99":4305.460013389587}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:06+00","p50":3809.75,"p95":4062.0999863147736,"p99":4316.2001876831055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:07+00","p50":3722.75,"p95":3998.999960899353,"p99":4158.300007820129}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:08+00","p50":3747.25,"p95":4049.49999833107,"p99":4103.600034713745}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:09+00","p50":3882.0,"p95":4048.1499946713448,"p99":4198.320045471191}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:10+00","p50":3923.75,"p95":4140.1999834775925,"p99":4209.960001468658}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:11+00","p50":3972.625,"p95":4166.0,"p99":4343.900180339813}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:12+00","p50":3981.625,"p95":4243.699975132942,"p99":4498.890126466751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:13+00","p50":3935.125,"p95":4239.599992513657,"p99":4321.300014972687}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:14+00","p50":3854.75,"p95":4153.599994063377,"p99":4404.8802881240845}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:15+00","p50":3809.0,"p95":4199.249972999096,"p99":4521.27010512352}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:16+00","p50":3753.75,"p95":4155.849997937679,"p99":4413.930179834366}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:17+00","p50":3732.0,"p95":3932.19997215271,"p99":4103.080107212067}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:18+00","p50":3770.25,"p95":4098.499972462654,"p99":4305.700097560883}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:19+00","p50":3689.25,"p95":4186.199967384338,"p99":4410.740236282349}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:20+00","p50":3772.25,"p95":4209.799936056137,"p99":4322.50021314621}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:21+00","p50":4043.75,"p95":4289.2499822974205,"p99":4353.750032186508}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:22+00","p50":4034.875,"p95":4265.299996256828,"p99":4331.90004491806}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:23+00","p50":4040.125,"p95":4489.0999583005905,"p99":4635.000151634216}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:24+00","p50":4052.5,"p95":4416.0,"p99":4515.160109996796}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:25+00","p50":3912.5,"p95":4366.699989914894,"p99":4669.700174808502}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:26+00","p50":3983.5,"p95":4166.199993133545,"p99":4270.300212860107}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:27+00","p50":4067.0,"p95":4233.799982428551,"p99":4339.120104789734}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:28+00","p50":4209.25,"p95":4434.0,"p99":4518.560173034668}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:29+00","p50":4234.5,"p95":4469.0,"p99":4687.500037193298}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:30+00","p50":4052.0,"p95":4279.299973964691,"p99":4384.160060882568}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:31+00","p50":3883.5,"p95":4106.44998639822,"p99":4164.790026426315}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:32+00","p50":3662.5,"p95":3866.99995136261,"p99":4143.400110244751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:33+00","p50":3663.0,"p95":3798.199977993965,"p99":3870.0001015663147}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:34+00","p50":3737.875,"p95":3971.7999844551086,"p99":4087.3100202083588}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:35+00","p50":3780.25,"p95":3977.599996328354,"p99":4236.360023498535}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:36+00","p50":3841.375,"p95":4012.0,"p99":4228.99013209343}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:37+00","p50":3723.25,"p95":3884.8999894857407,"p99":3999.290200471878}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:38+00","p50":3785.25,"p95":4037.499991416931,"p99":4130.6602210998535}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:39+00","p50":3860.125,"p95":4091.049997985363,"p99":4283.520148277283}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:40+00","p50":3826.25,"p95":4033.2999980449677,"p99":4207.100054740906}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:41+00","p50":3758.875,"p95":4277.349994957447,"p99":4492.470111608505}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:42+00","p50":4006.25,"p95":4362.799988150597,"p99":4779.360226154327}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:43+00","p50":3941.5,"p95":4347.799966096878,"p99":4859.68026971817}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:44+00","p50":3971.0,"p95":4429.199998021126,"p99":4449.680003166199}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:45+00","p50":4004.0,"p95":4684.0,"p99":4779.500356674194}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:46+00","p50":3841.5,"p95":4735.599987983704,"p99":5164.820402145386}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:47+00","p50":3701.0,"p95":4457.599954366684,"p99":4762.8005475997925}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:48+00","p50":3872.25,"p95":4514.849944531918,"p99":4965.450006723404}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:49+00","p50":4150.0,"p95":4711.849981248379,"p99":4902.650265932083}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:50+00","p50":3839.25,"p95":4612.099994421005,"p99":5044.340611457825}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:51+00","p50":3963.375,"p95":4596.5999920368195,"p99":5144.410043001175}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:52+00","p50":4034.0,"p95":4601.999965667725,"p99":4852.200141906738}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:53+00","p50":4035.375,"p95":4561.549994528294,"p99":4953.540411472321}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:54+00","p50":4090.0,"p95":4640.199996709824,"p99":5038.840239524841}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:55+00","p50":4226.25,"p95":4770.399990558624,"p99":4842.020049095154}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:56+00","p50":4394.0,"p95":4942.999993562698,"p99":5273.900110721588}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:57+00","p50":4314.25,"p95":4887.999962806702,"p99":5368.000049591064}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:58+00","p50":4279.25,"p95":4845.849997937679,"p99":4949.9400362968445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:33:59+00","p50":4125.75,"p95":4693.799988985062,"p99":4754.760008811951}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:00+00","p50":3975.0,"p95":4484.899998188019,"p99":4919.220346450806}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:01+00","p50":3890.5,"p95":4401.0,"p99":4745.340165138245}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:02+00","p50":3929.5,"p95":4259.199996471405,"p99":4658.380026817322}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:03+00","p50":3953.0,"p95":4358.399976968765,"p99":4599.480049133301}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:04+00","p50":3947.375,"p95":4384.749975979328,"p99":4521.30007982254}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:05+00","p50":3775.25,"p95":4359.999978303909,"p99":4587.800060749054}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:06+00","p50":3679.0,"p95":4233.199996709824,"p99":4335.480038166046}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:07+00","p50":3778.75,"p95":4290.0,"p99":4378.680011749268}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:08+00","p50":3780.5,"p95":4450.199993133545,"p99":4994.420009613037}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:09+00","p50":3969.0,"p95":4598.549997866154,"p99":4978.570114374161}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:10+00","p50":3864.5,"p95":4575.999981880188,"p99":4649.88000869751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:11+00","p50":3791.0,"p95":4520.799996376038,"p99":4616.80001449585}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:12+00","p50":3860.5,"p95":4730.599987983704,"p99":5105.900632858276}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:13+00","p50":3817.5,"p95":4650.599986076355,"p99":4809.040767192841}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:14+00","p50":3972.75,"p95":4779.79999256134,"p99":5169.840127944946}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:15+00","p50":3746.5,"p95":4710.199995994568,"p99":4765.100008010864}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:16+00","p50":4042.125,"p95":4776.949957191944,"p99":5215.720273971558}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:17+00","p50":3768.625,"p95":4810.049986064434,"p99":4859.1500079631805}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:18+00","p50":3762.0,"p95":4681.599971294403,"p99":5169.9200229644775}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:19+00","p50":3779.375,"p95":4429.099975466728,"p99":4867.740221500397}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:20+00","p50":3620.25,"p95":4606.949944317341,"p99":4688.320026397705}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:21+00","p50":3703.125,"p95":4388.199985027313,"p99":5003.82026052475}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:22+00","p50":3727.25,"p95":4421.699958324432,"p99":4512.96000289917}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:23+00","p50":3875.25,"p95":4596.79994893074,"p99":4630.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:24+00","p50":3845.5,"p95":4551.199978232384,"p99":4947.640666484833}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:25+00","p50":3798.5,"p95":4540.999988555908,"p99":4974.700630187988}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:26+00","p50":3839.875,"p95":4544.149984657764,"p99":4664.000409126282}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:27+00","p50":3984.875,"p95":4845.349968969822,"p99":4937.690408945084}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:28+00","p50":3997.5,"p95":4760.349952042103,"p99":4850.790680646896}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:29+00","p50":4096.125,"p95":4728.249987065792,"p99":5155.6000118255615}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:30+00","p50":4063.125,"p95":4737.799992322922,"p99":4784.780003070831}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:31+00","p50":3750.0,"p95":4657.199995756149,"p99":5099.920909881592}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:32+00","p50":3821.25,"p95":4525.999969601631,"p99":5025.000080108643}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:33+00","p50":3526.25,"p95":4410.249998033047,"p99":4825.800704956055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:34+00","p50":3715.125,"p95":4360.149998247623,"p99":4413.1800084114075}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:35+00","p50":3754.5,"p95":4417.999981880188,"p99":4801.800068855286}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:36+00","p50":3653.0,"p95":4356.999988555908,"p99":4730.200576782227}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:37+00","p50":3659.25,"p95":4340.2999947071075,"p99":4873.8400592803955}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:38+00","p50":3744.0,"p95":4546.899996638298,"p99":4996.980029582977}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:39+00","p50":3891.75,"p95":4519.099987983704,"p99":4923.060138702393}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:40+00","p50":3929.375,"p95":4596.899963736534,"p99":5031.8405866622925}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:41+00","p50":3831.5,"p95":4624.099957942963,"p99":5074.720073699951}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:42+00","p50":4009.625,"p95":4626.649995028973,"p99":4700.660007953644}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:43+00","p50":3808.25,"p95":4593.099987983704,"p99":5075.180072784424}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:44+00","p50":3845.5,"p95":4500.8999853134155,"p99":4555.960090637207}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:45+00","p50":3915.25,"p95":4507.349994957447,"p99":4600.810012102127}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:46+00","p50":3892.5,"p95":4662.399991989136,"p99":5012.000560760498}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:47+00","p50":3701.5,"p95":4560.799988150597,"p99":5354.6812472343445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:48+00","p50":3899.0,"p95":4659.799976825714,"p99":6050.800478935242}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:49+00","p50":3850.5,"p95":4831.399900436401,"p99":7147.2404861450195}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:50+00","p50":3883.0,"p95":4801.7499905228615,"p99":5315.350735425949}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:51+00","p50":4263.625,"p95":4953.949998438358,"p99":5066.660017490387}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:52+00","p50":4364.25,"p95":5223.099960923195,"p99":5291.57008099556}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:53+00","p50":4163.5,"p95":5313.3999853134155,"p99":5929.120032310486}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:54+00","p50":4001.375,"p95":5105.499996066093,"p99":5768.501148700714}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:55+00","p50":3928.75,"p95":4952.0,"p99":5004.8600606918335}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:56+00","p50":3719.0,"p95":5103.04995816946,"p99":5628.410033464432}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:57+00","p50":4237.375,"p95":5057.049987733364,"p99":5707.230197668076}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:58+00","p50":4231.375,"p95":5084.499996304512,"p99":5617.500724315643}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:34:59+00","p50":4046.625,"p95":4858.6499781012535,"p99":5337.300175189972}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:00+00","p50":3772.0,"p95":4932.499989628792,"p99":5574.650211572647}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:01+00","p50":4093.875,"p95":4680.899996161461,"p99":4716.350023031235}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:02+00","p50":3817.25,"p95":4573.399984359741,"p99":4796.52028465271}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:03+00","p50":3751.875,"p95":4285.849993884563,"p99":4359.330044031143}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:04+00","p50":3715.0,"p95":4180.499979019165,"p99":4392.00016784668}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:05+00","p50":3629.25,"p95":4080.899998188019,"p99":4496.540033340454}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:06+00","p50":3723.875,"p95":4125.349998056889,"p99":4414.820444583893}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:07+00","p50":3740.5,"p95":4205.599984169006,"p99":4257.840001583099}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:08+00","p50":3760.875,"p95":4343.199993848801,"p99":4544.730754137039}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:09+00","p50":3933.25,"p95":4450.299998044968,"p99":4808.640037536621}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:10+00","p50":3904.375,"p95":4387.199957370758,"p99":4496.630373716354}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:11+00","p50":3951.0,"p95":4466.899986982346,"p99":4794.72013092041}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:12+00","p50":3829.625,"p95":4584.099995970726,"p99":4703.090143442154}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:13+00","p50":3801.5,"p95":4236.249966561794,"p99":4315.100009441376}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:14+00","p50":3756.5,"p95":4235.599996328354,"p99":4655.0005140304565}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:15+00","p50":3713.0,"p95":4283.499967575073,"p99":4601.000076293945}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:16+00","p50":3764.375,"p95":4125.549986898899,"p99":4409.030405759811}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:17+00","p50":3811.75,"p95":4293.999992847443,"p99":4380.001082897186}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:18+00","p50":3878.5,"p95":4423.899998188019,"p99":4730.660024642944}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:19+00","p50":3889.5,"p95":4526.1999942064285,"p99":4839.160435676575}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:20+00","p50":3833.25,"p95":4454.899984121323,"p99":4983.880485534668}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:21+00","p50":3794.625,"p95":4624.199985980988,"p99":4722.820412158966}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:22+00","p50":4123.0,"p95":4774.499974131584,"p99":4865.500014781952}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:23+00","p50":3996.5,"p95":4810.4999812841415,"p99":4876.95002245903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:24+00","p50":3932.375,"p95":4599.999992609024,"p99":4664.550013303757}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:25+00","p50":3868.5,"p95":4584.1999834775925,"p99":4688.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:26+00","p50":3968.0,"p95":4624.799987792969,"p99":4842.40087890625}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:27+00","p50":3991.5,"p95":4941.999985694885,"p99":5063.000414848328}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:28+00","p50":4171.5,"p95":4992.199980974197,"p99":5743.440294265747}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:29+00","p50":4085.25,"p95":5033.499990224838,"p99":5375.040491104126}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:30+00","p50":4275.125,"p95":5243.849998176098,"p99":5858.750109434128}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:31+00","p50":3708.0,"p95":5302.399966955185,"p99":5458.960074901581}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:32+00","p50":4213.5,"p95":5170.999988555908,"p99":5822.4010009765625}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:33+00","p50":3490.125,"p95":5090.699796557426,"p99":5878.530118227005}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:34+00","p50":4116.125,"p95":5050.499981999397,"p99":5110.72004032135}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:35+00","p50":3826.25,"p95":4959.599971294403,"p99":5081.740136146545}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:36+00","p50":3585.875,"p95":4820.849998176098,"p99":4901.650065660477}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:37+00","p50":3860.5,"p95":4968.399994492531,"p99":5591.680011749268}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:38+00","p50":3515.875,"p95":5019.199992895126,"p99":5536.460207462311}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:39+00","p50":4234.0,"p95":5163.099989056587,"p99":5261.700014591217}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:40+00","p50":3578.25,"p95":5180.999998211861,"p99":5288.00001001358}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:41+00","p50":3645.625,"p95":5357.249990403652,"p99":5908.53088593483}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:42+00","p50":4340.5,"p95":5304.549980461597,"p99":5402.800966262817}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:43+00","p50":3720.25,"p95":5352.599987030029,"p99":6145.720127105713}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:44+00","p50":4337.25,"p95":5423.549937546253,"p99":6147.430004835129}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:45+00","p50":4158.5,"p95":5225.79999434948,"p99":5890.5209131240845}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:46+00","p50":3855.0,"p95":5103.199995994568,"p99":5667.701017379761}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:47+00","p50":3376.0,"p95":5010.0,"p99":5758.600214004517}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:48+00","p50":3909.375,"p95":4917.649983584881,"p99":5062.430027723312}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:49+00","p50":3968.0,"p95":4958.599974274635,"p99":5062.520044326782}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:50+00","p50":3442.375,"p95":4902.449980199337,"p99":5612.200115203857}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:51+00","p50":4073.25,"p95":5081.799989700317,"p99":5608.460056304932}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:52+00","p50":3627.5,"p95":5167.199996709824,"p99":5826.400092124939}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:53+00","p50":3775.75,"p95":5251.799992322922,"p99":5358.040055274963}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:54+00","p50":4138.625,"p95":5167.0,"p99":5908.450023889542}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:55+00","p50":4299.5,"p95":5252.999996185303,"p99":5798.900856018066}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:56+00","p50":3525.25,"p95":5326.249992311001,"p99":5425.020076274872}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:57+00","p50":3761.75,"p95":5267.199986457825,"p99":5423.681044101715}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:58+00","p50":4487.5,"p95":5409.099989652634,"p99":5626.480825424194}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:35:59+00","p50":4175.0,"p95":5478.149986565113,"p99":6087.730855226517}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:00+00","p50":4575.875,"p95":5416.44999474287,"p99":5466.630029439926}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:01+00","p50":4432.125,"p95":5388.099977135658,"p99":5490.430145025253}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:02+00","p50":3898.75,"p95":5309.999948263168,"p99":6057.550368070602}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:03+00","p50":4103.25,"p95":5220.0,"p99":5296.440020561218}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:04+00","p50":3936.25,"p95":4946.8999963998795,"p99":5092.340095043182}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:05+00","p50":3962.25,"p95":4920.799942612648,"p99":5429.560726642609}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:06+00","p50":3686.0,"p95":4713.099975824356,"p99":5307.360883712769}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:07+00","p50":3674.25,"p95":4727.39999628067,"p99":4928.72013092041}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:08+00","p50":3937.25,"p95":4815.199969291687,"p99":5464.5000767707825}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:09+00","p50":3978.875,"p95":4724.5999846458435,"p99":4803.060082912445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:10+00","p50":4023.125,"p95":4732.349994242191,"p99":4767.020027637482}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:11+00","p50":3516.25,"p95":4758.749971926212,"p99":4828.650007486343}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:12+00","p50":3976.0,"p95":4860.19998562336,"p99":5705.760844707489}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:13+00","p50":3846.5,"p95":4802.999981760979,"p99":4889.80001783371}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:14+00","p50":3833.25,"p95":4742.649973809719,"p99":5155.200032234192}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:15+00","p50":3788.75,"p95":4601.799984455109,"p99":4970.640422821045}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:16+00","p50":3372.0,"p95":4700.799976825714,"p99":5059.920516014099}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:17+00","p50":3544.0,"p95":4671.999932765961,"p99":5308.270004034042}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:18+00","p50":3961.625,"p95":4990.899996876717,"p99":5194.110710859299}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:19+00","p50":4072.25,"p95":5018.899998188019,"p99":5127.800086975098}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:20+00","p50":4156.0,"p95":5117.0499984622,"p99":5282.070697546005}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:21+00","p50":4438.5,"p95":5432.699986696243,"p99":5986.9400815963745}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:22+00","p50":4368.75,"p95":5729.299957513809,"p99":5843.860033988953}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:23+00","p50":4841.0,"p95":5886.749982178211,"p99":6563.200254440308}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:24+00","p50":4478.0,"p95":5933.49998998642,"p99":6036.74001121521}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:25+00","p50":4330.5,"p95":5872.949967205524,"p99":6546.350081205368}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:26+00","p50":4738.75,"p95":6019.399987816811,"p99":6158.720721244812}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:27+00","p50":4160.75,"p95":5609.599992752075,"p99":5676.88000869751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:28+00","p50":4555.75,"p95":5691.849995315075,"p99":5908.370903253555}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:29+00","p50":3989.0,"p95":5588.899961948395,"p99":6492.001087188721}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:30+00","p50":4278.0,"p95":5508.999996185303,"p99":6325.100028991699}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:31+00","p50":4215.625,"p95":5365.7999539375305,"p99":5992.660912036896}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:32+00","p50":3654.25,"p95":5198.599992752075,"p99":5274.80001449585}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:33+00","p50":3601.75,"p95":5260.249990403652,"p99":5343.920042991638}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:34+00","p50":4181.75,"p95":4979.599994778633,"p99":5703.240669727325}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:35+00","p50":3663.5,"p95":5178.499979019165,"p99":5219.700004577637}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:36+00","p50":3540.75,"p95":5082.499996304512,"p99":5203.550101995468}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:37+00","p50":4036.0,"p95":5194.0,"p99":5892.220986366272}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:38+00","p50":3905.5,"p95":5056.399989128113,"p99":5919.140062332153}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:39+00","p50":3588.375,"p95":5001.349994242191,"p99":5108.820058345795}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:40+00","p50":4206.75,"p95":5068.399998307228,"p99":5502.560077190399}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:41+00","p50":4142.25,"p95":5048.449994027615,"p99":5613.940984249115}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:42+00","p50":3670.0,"p95":5113.249993503094,"p99":5416.350452184677}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:43+00","p50":4432.0,"p95":5181.599960446358,"p99":5285.440048217773}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:44+00","p50":3911.5,"p95":5309.899994850159,"p99":5481.560035705566}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:45+00","p50":4228.25,"p95":5336.4999388456345,"p99":5858.280052185059}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:46+00","p50":3901.5,"p95":4979.799975395203,"p99":5404.620785713196}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:47+00","p50":3849.0,"p95":4789.099955677986,"p99":5395.840016365051}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:48+00","p50":3903.0,"p95":4651.399993419647,"p99":4764.440048694611}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:49+00","p50":3821.125,"p95":4650.0,"p99":4949.300523281097}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:50+00","p50":3875.25,"p95":4579.099994421005,"p99":5077.5800104141235}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:51+00","p50":3925.25,"p95":4726.599974632263,"p99":4930.380117416382}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:52+00","p50":3835.25,"p95":4728.699958324432,"p99":4796.980001449585}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:53+00","p50":3802.375,"p95":4491.39984869957,"p99":5060.140729427338}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:54+00","p50":3941.375,"p95":4651.999985218048,"p99":5102.350521802902}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:55+00","p50":3666.125,"p95":4560.999979257584,"p99":4630.500013828278}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:56+00","p50":3892.0,"p95":4561.9999541044235,"p99":4907.640270233154}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:57+00","p50":4087.0,"p95":4605.449968516827,"p99":4742.240641593933}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:58+00","p50":4103.625,"p95":4598.999993085861,"p99":4681.3500373363495}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:36:59+00","p50":4132.5,"p95":4611.199996471405,"p99":4833.5802526474}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:00+00","p50":4035.0,"p95":4423.199995994568,"p99":4492.48002243042}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:01+00","p50":3950.125,"p95":4391.699996113777,"p99":4516.400031089783}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:02+00","p50":3808.25,"p95":4147.999974966049,"p99":4325.000575065613}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:03+00","p50":3741.0,"p95":4050.1999868154526,"p99":4340.040018081665}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:04+00","p50":3802.0,"p95":3949.1999962329865,"p99":4013.040018081665}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:05+00","p50":3848.625,"p95":4014.4999960660934,"p99":4094.3500487804413}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:06+00","p50":3807.0,"p95":4018.9999982118607,"p99":4224.000236034393}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:07+00","p50":3829.25,"p95":4053.4499940276146,"p99":4302.890132188797}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:08+00","p50":3832.5,"p95":4177.199996709824,"p99":4317.520225048065}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:09+00","p50":4018.5,"p95":4309.999944925308,"p99":4509.6000146865845}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:10+00","p50":3936.5,"p95":4432.849994599819,"p99":4714.030139684677}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:11+00","p50":3960.125,"p95":4379.799992322922,"p99":4472.640116691589}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:12+00","p50":3946.0,"p95":4474.999977111816,"p99":4650.400207519531}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:13+00","p50":3785.75,"p95":4335.299977064133,"p99":4451.0404262542725}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:14+00","p50":3798.0,"p95":4280.099997997284,"p99":4683.100008010864}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:15+00","p50":3796.375,"p95":4379.499929785728,"p99":4474.600070953369}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:16+00","p50":3795.125,"p95":4338.2999893426895,"p99":4401.920272827148}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:17+00","p50":3788.0,"p95":4281.999990105629,"p99":4379.760022163391}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:18+00","p50":3854.125,"p95":4352.649998366833,"p99":4817.300666332245}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:19+00","p50":3962.375,"p95":4497.799985408783,"p99":4564.310033559799}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:20+00","p50":3904.0,"p95":4429.399916887283,"p99":4943.840120315552}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:21+00","p50":3931.875,"p95":4488.549977838993,"p99":4774.000136375427}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:22+00","p50":4007.25,"p95":4625.899988770485,"p99":4861.780218601227}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:23+00","p50":3796.0,"p95":4501.199951648712,"p99":4868.460236549377}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:24+00","p50":3911.75,"p95":4361.499990701675,"p99":4433.7200565338135}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:25+00","p50":3822.0,"p95":4436.2999947071075,"p99":4749.920206069946}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:26+00","p50":3845.25,"p95":4478.699950218201,"p99":4694.3203048706055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:27+00","p50":4004.5,"p95":4560.049998223782,"p99":4876.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:28+00","p50":4028.0,"p95":4550.899996638298,"p99":5053.210092782974}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:29+00","p50":4186.375,"p95":4740.749998390675,"p99":4835.400149345398}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:30+00","p50":4197.25,"p95":4873.099983692169,"p99":4961.880081176758}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:31+00","p50":4057.75,"p95":4824.2999856472015,"p99":4940.700024604797}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:32+00","p50":3978.5,"p95":4362.799985408783,"p99":4409.610018968582}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:33+00","p50":3826.125,"p95":4200.199992418289,"p99":4268.1900136470795}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:34+00","p50":3719.25,"p95":4004.4499980807304,"p99":4416.330609560013}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:35+00","p50":3822.5,"p95":3990.0,"p99":4053.240035533905}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:36+00","p50":3816.5,"p95":4094.7999843358994,"p99":4378.760061264038}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:37+00","p50":3842.0,"p95":4080.1999942064285,"p99":4176.48010969162}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:38+00","p50":3851.0,"p95":4040.7999963760376,"p99":4174.860010147095}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:39+00","p50":3878.125,"p95":4165.749967157841,"p99":4250.550070524216}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:40+00","p50":3975.75,"p95":4276.0,"p99":4460.000193595886}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:41+00","p50":3902.0,"p95":4234.0,"p99":4441.440085887909}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:42+00","p50":3867.375,"p95":4311.0,"p99":4543.320097923279}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:43+00","p50":4052.25,"p95":4533.999989271164,"p99":4837.000011444092}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:44+00","p50":4022.0,"p95":4480.999996185303,"p99":4871.9001541137695}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:45+00","p50":3914.75,"p95":4411.299983263016,"p99":4561.540060997009}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:46+00","p50":3883.625,"p95":4551.749991238117,"p99":4951.540165424347}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:47+00","p50":3788.25,"p95":4530.99991941452,"p99":5003.240167617798}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:48+00","p50":3792.25,"p95":4369.999998211861,"p99":4445.0000014305115}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:49+00","p50":3913.875,"p95":4534.199986934662,"p99":4623.500065326691}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:50+00","p50":4003.75,"p95":4770.99999153614,"p99":5014.360260009766}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:51+00","p50":4140.375,"p95":4955.249992311001,"p99":5012.63000369072}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:52+00","p50":4360.625,"p95":4995.0499984622,"p99":5156.030544996262}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:53+00","p50":4308.875,"p95":5029.849983155727,"p99":5338.6400718688965}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:54+00","p50":4045.5,"p95":5049.2999856472015,"p99":5098.800016403198}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:55+00","p50":4125.0,"p95":5094.899994850159,"p99":5677.660152435303}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:56+00","p50":3942.75,"p95":5090.2499822974205,"p99":5313.101073741913}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:57+00","p50":3931.5,"p95":5091.999979972839,"p99":5244.700970649719}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:58+00","p50":4284.5,"p95":5282.0,"p99":5300.680006980896}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:37:59+00","p50":4374.0,"p95":5387.0,"p99":5462.520054340363}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:00+00","p50":4423.0,"p95":5355.499990463257,"p99":5401.600021362305}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:01+00","p50":4390.25,"p95":5318.8999963998795,"p99":5357.910012960434}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:02+00","p50":3752.625,"p95":5421.599993467331,"p99":5601.930994272232}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:03+00","p50":4171.875,"p95":5534.999985218048,"p99":6329.751012563705}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:04+00","p50":4134.0,"p95":5168.499974131584,"p99":6115.701368808746}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:05+00","p50":3641.0,"p95":5121.749954998493,"p99":5639.8905918598175}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:06+00","p50":3533.25,"p95":5233.599989414215,"p99":5329.1607875823975}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:07+00","p50":4414.625,"p95":5327.949982941151,"p99":5585.380330562592}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:08+00","p50":3593.0,"p95":5225.0,"p99":5957.000073432922}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:09+00","p50":3745.625,"p95":4994.399986743927,"p99":5810.470102071762}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:10+00","p50":4201.875,"p95":5168.249956548214,"p99":5276.500605106354}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:11+00","p50":4296.5,"p95":5340.749991476536,"p99":5912.410085916519}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:12+00","p50":4123.75,"p95":5249.699898719788,"p99":5454.86079788208}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:13+00","p50":4207.375,"p95":5221.299988627434,"p99":5798.98087644577}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:14+00","p50":4300.75,"p95":5240.499980568886,"p99":5800.990586042404}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:15+00","p50":4021.875,"p95":5003.999992609024,"p99":5688.600780487061}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:16+00","p50":3974.125,"p95":4904.999984264374,"p99":5508.500204563141}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:17+00","p50":3907.0,"p95":4467.799996376038,"p99":4747.92000579834}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:18+00","p50":3849.0,"p95":4483.999998211861,"p99":4617.000050067902}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:19+00","p50":3800.5,"p95":4366.599995851517,"p99":4460.68002986908}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:20+00","p50":3755.0,"p95":4394.649983346462,"p99":4475.880067825317}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:21+00","p50":4082.25,"p95":4514.899996638298,"p99":4617.120494842529}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:22+00","p50":4141.625,"p95":4712.099927067757,"p99":5238.570173501968}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:23+00","p50":4075.875,"p95":4613.849978148937,"p99":4973.660502910614}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:24+00","p50":4117.75,"p95":4893.199978113174,"p99":5070.59006857872}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:25+00","p50":4087.5,"p95":4945.199996471405,"p99":4981.660611152649}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:26+00","p50":3976.25,"p95":4914.0,"p99":4969.400048732758}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:27+00","p50":4089.25,"p95":4930.499981641769,"p99":5100.700583457947}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:28+00","p50":4155.625,"p95":5155.599986553192,"p99":5310.480903625488}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:29+00","p50":4171.5,"p95":5227.399996042252,"p99":5770.3609166145325}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:30+00","p50":4215.0,"p95":5049.799979805946,"p99":5128.360023498535}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:31+00","p50":3727.0,"p95":5163.1999834775925,"p99":5221.520017623901}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:32+00","p50":4023.25,"p95":5076.899996161461,"p99":5715.53088593483}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:33+00","p50":3713.0,"p95":4990.199995994568,"p99":5625.781007766724}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:34+00","p50":4059.875,"p95":5056.149949371815,"p99":5609.040271759033}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:35+00","p50":3589.0,"p95":5088.599996328354,"p99":5152.680011749268}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:36+00","p50":4089.375,"p95":5014.499979138374,"p99":5584.001128196716}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:37+00","p50":3988.0,"p95":4981.0,"p99":5665.30002117157}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:38+00","p50":3495.0,"p95":5195.849966704845,"p99":5284.270012617111}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:39+00","p50":4107.875,"p95":5404.349964439869,"p99":6038.040041923523}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:40+00","p50":3712.75,"p95":5021.999992132187,"p99":5499.450719118118}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:41+00","p50":3990.25,"p95":5085.249992311001,"p99":5281.290675401688}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:42+00","p50":4315.0,"p95":5241.749973356724,"p99":5882.490211725235}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:43+00","p50":4062.375,"p95":5281.349998056889,"p99":5978.67115187645}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:44+00","p50":3830.25,"p95":5100.3999791145325,"p99":5295.520957946777}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:45+00","p50":4365.25,"p95":5167.999930262566,"p99":5665.00001001358}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:46+00","p50":3988.75,"p95":5150.299996256828,"p99":5903.95002245903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:47+00","p50":3847.375,"p95":4891.899996161461,"p99":5571.8109657764435}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:48+00","p50":3974.5,"p95":4876.499996542931,"p99":4948.100085735321}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:49+00","p50":3955.5,"p95":5018.999974608421,"p99":5721.320106983185}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:50+00","p50":3939.0,"p95":5065.9999878406525,"p99":5135.600051879883}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:51+00","p50":3720.5,"p95":4909.899980068207,"p99":5603.700963973999}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:52+00","p50":4130.0,"p95":5061.599992752075,"p99":5121.720020294189}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:53+00","p50":3931.75,"p95":5096.249998271465,"p99":5647.450316667557}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:54+00","p50":3781.75,"p95":5210.699987769127,"p99":5277.54004240036}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:55+00","p50":4219.75,"p95":5131.499996542931,"p99":5875.850078821182}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:56+00","p50":3469.625,"p95":5327.299990057945,"p99":5531.290449380875}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:57+00","p50":4399.75,"p95":5320.49998152256,"p99":5434.900121212006}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:58+00","p50":4286.0,"p95":5417.799995064735,"p99":5519.800710678101}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:38:59+00","p50":4430.0,"p95":5298.749986231327,"p99":5950.751093626022}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:00+00","p50":3588.0,"p95":5338.999981164932,"p99":5386.9600195884705}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:01+00","p50":3603.125,"p95":5136.2999213933945,"p99":5861.040940284729}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:02+00","p50":3566.0,"p95":5233.399996995926,"p99":5988.9600348472595}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:03+00","p50":4156.375,"p95":5294.399983882904,"p99":5959.630037069321}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:04+00","p50":3597.75,"p95":5037.999979496002,"p99":5893.140349388123}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:05+00","p50":4163.0,"p95":5027.8999745845795,"p99":5711.020089149475}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:06+00","p50":3487.5,"p95":5206.9999923706055,"p99":5270.100044250488}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:07+00","p50":3872.375,"p95":4882.0,"p99":5082.800058364868}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:08+00","p50":3990.375,"p95":4804.749986231327,"p99":5222.7006640434265}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:09+00","p50":3752.25,"p95":4835.249950110912,"p99":5235.100410938263}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:10+00","p50":4137.625,"p95":4923.349994242191,"p99":5308.750115156174}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:11+00","p50":3728.25,"p95":4866.249973714352,"p99":5094.250385522842}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:12+00","p50":3830.375,"p95":4692.049986064434,"p99":4775.470014333725}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:13+00","p50":3813.5,"p95":4700.499980807304,"p99":5267.460328578949}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:14+00","p50":3784.25,"p95":4556.999995708466,"p99":4587.700036048889}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:15+00","p50":3766.625,"p95":4440.249991118908,"p99":4577.760392189026}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:16+00","p50":3509.625,"p95":4367.099970459938,"p99":4413.870032072067}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:17+00","p50":3579.5,"p95":4390.199939727783,"p99":4854.200655460358}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:18+00","p50":3648.0,"p95":4328.999988555908,"p99":4391.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:19+00","p50":3721.0,"p95":4389.749971926212,"p99":4521.590094327927}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:20+00","p50":3758.75,"p95":4442.7999712228775,"p99":4615.160239696503}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:21+00","p50":4007.5,"p95":4754.499983668327,"p99":4854.820018291473}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:22+00","p50":4038.75,"p95":4845.199998259544,"p99":4901.120526313782}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:23+00","p50":4051.0,"p95":4939.1999942064285,"p99":5016.040012359619}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:24+00","p50":4182.0,"p95":4905.7999930381775,"p99":5543.320115566254}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:25+00","p50":3949.875,"p95":4992.549980461597,"p99":5067.380196094513}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:26+00","p50":3912.25,"p95":4982.099996447563,"p99":5751.680949211121}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:27+00","p50":4160.5,"p95":5076.249991834164,"p99":5612.640167236328}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:28+00","p50":4049.75,"p95":5145.0,"p99":5729.000034809113}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:29+00","p50":3734.5,"p95":5179.849997937679,"p99":5605.570727586746}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:30+00","p50":4279.75,"p95":5085.649980723858,"p99":5621.361138343811}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:31+00","p50":3644.875,"p95":5022.299987912178,"p99":5114.240006446838}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:32+00","p50":3804.5,"p95":4654.399993777275,"p99":4751.440031528473}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:33+00","p50":3832.5,"p95":4499.5999846458435,"p99":5141.061004161835}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:34+00","p50":3857.0,"p95":4489.999964952469,"p99":4715.850273370743}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:35+00","p50":3545.5,"p95":4348.599983215332,"p99":4774.740673065186}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:36+00","p50":3760.875,"p95":4352.499974131584,"p99":4731.000029563904}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:37+00","p50":3772.5,"p95":4501.999971747398,"p99":4716.320219993591}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:38+00","p50":3740.5,"p95":4565.799996614456,"p99":4665.920608043671}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:39+00","p50":3996.0,"p95":4625.249995172024,"p99":5226.50003862381}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:40+00","p50":4067.0,"p95":4612.399992465973,"p99":4782.640100955963}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:41+00","p50":3947.5,"p95":4650.499998092651,"p99":4777.900047302246}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:42+00","p50":3761.0,"p95":4749.199996232986,"p99":4825.280013561249}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:43+00","p50":3802.25,"p95":4626.399997830391,"p99":4787.64012670517}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:44+00","p50":3779.5,"p95":4299.499968290329,"p99":4738.400339126587}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:45+00","p50":3906.5,"p95":4316.0,"p99":4475.200128078461}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:46+00","p50":3786.5,"p95":4070.999984741211,"p99":4399.000457763672}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:47+00","p50":3856.0,"p95":4207.149998247623,"p99":4390.460114955902}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:48+00","p50":3883.25,"p95":4188.799979805946,"p99":4354.160140991211}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:49+00","p50":3890.125,"p95":4035.099996447563,"p99":4280.4702088832855}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:50+00","p50":3888.5,"p95":4059.4999828338623,"p99":4124.10001373291}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:51+00","p50":3910.75,"p95":4121.149979412556,"p99":4317.500074863434}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:52+00","p50":3899.875,"p95":4079.249972999096,"p99":4282.750180006027}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:53+00","p50":3821.0,"p95":4043.4999980926514,"p99":4208.300117492676}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:54+00","p50":3716.0,"p95":4129.299913883209,"p99":4529.820402145386}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:55+00","p50":3893.75,"p95":4100.099996685982,"p99":4381.490078210831}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:56+00","p50":3919.0,"p95":4271.999923944473,"p99":4456.050001382828}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:57+00","p50":4046.375,"p95":4356.649995028973,"p99":4428.630043745041}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:58+00","p50":4082.0,"p95":4493.199988484383,"p99":4561.040055274963}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:39:59+00","p50":4173.5,"p95":4646.199967980385,"p99":5084.560027122498}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:00+00","p50":4080.125,"p95":4632.249990642071,"p99":4922.020320415497}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:01+00","p50":4017.25,"p95":4443.999947071075,"p99":4679.340165138245}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:02+00","p50":3956.0,"p95":4371.3999980688095,"p99":4691.760196208954}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:03+00","p50":3847.375,"p95":4031.949978888035,"p99":4065.4500076770782}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:04+00","p50":3776.5,"p95":4055.4499940276146,"p99":4173.050055742264}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:05+00","p50":3761.0,"p95":3977.2999947071075,"p99":4021.060004234314}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:06+00","p50":3721.875,"p95":3911.2499901652336,"p99":4108.350017309189}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:07+00","p50":3818.0,"p95":4018.3999853134155,"p99":4141.000073432922}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:08+00","p50":3857.0,"p95":4202.099960923195,"p99":4396.510214567184}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:09+00","p50":3772.375,"p95":4176.499996066093,"p99":4257.100009441376}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:10+00","p50":3905.0,"p95":4269.849981248379,"p99":4585.9100177288055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:11+00","p50":3929.625,"p95":4512.699968457222,"p99":4689.380344867706}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:12+00","p50":3966.125,"p95":4465.599992990494,"p99":4565.380485057831}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:13+00","p50":3976.0,"p95":4516.799973964691,"p99":4596.220019340515}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:14+00","p50":3951.75,"p95":4629.699960947037,"p99":5020.620406150818}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:15+00","p50":3944.25,"p95":4670.0,"p99":5171.840899467468}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:16+00","p50":3866.25,"p95":4649.3999853134155,"p99":5095.40013217926}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:17+00","p50":3931.5,"p95":4607.3999980688095,"p99":5124.400007724762}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:18+00","p50":3835.375,"p95":4703.2999967336655,"p99":5212.140101909637}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:19+00","p50":4217.5,"p95":4912.999975204468,"p99":5120.200447559357}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:20+00","p50":3981.625,"p95":5050.349998056889,"p99":5440.640111923218}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:21+00","p50":4078.5,"p95":4821.44999474287,"p99":4842.24001121521}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:22+00","p50":4114.5,"p95":4866.299961447716,"p99":5064.010093927383}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:23+00","p50":3950.0,"p95":5005.599985122681,"p99":5557.100394248962}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:24+00","p50":3978.0,"p95":4752.199951648712,"p99":5408.700304985046}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:25+00","p50":3854.5,"p95":4590.799976110458,"p99":4773.770189523697}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:26+00","p50":3987.5,"p95":4839.499995708466,"p99":5099.900461196899}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:27+00","p50":3984.125,"p95":4924.749968588352,"p99":5531.35081744194}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:28+00","p50":3988.0,"p95":4791.699998140335,"p99":5343.440782546997}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:29+00","p50":3944.375,"p95":4814.749981224537,"p99":5419.250065088272}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:30+00","p50":3782.75,"p95":4946.0999982357025,"p99":5103.420453071594}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:31+00","p50":3843.75,"p95":4943.249990403652,"p99":5587.311042547226}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:32+00","p50":3742.375,"p95":4869.699996352196,"p99":4959.160040855408}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:33+00","p50":3902.0,"p95":4842.44998806715,"p99":5485.450184106827}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:34+00","p50":3930.875,"p95":4956.499948859215,"p99":5694.050209283829}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:35+00","p50":3524.25,"p95":5009.799991369247,"p99":5747.380176067352}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:36+00","p50":3426.5,"p95":4930.799992799759,"p99":5748.001008033752}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:37+00","p50":3956.5,"p95":5109.399950504303,"p99":5795.420075893402}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:38+00","p50":3417.875,"p95":4965.549995243549,"p99":4989.190008878708}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:39+00","p50":4077.25,"p95":5031.599992752075,"p99":5807.381059646606}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:40+00","p50":4008.5,"p95":5109.199991703033,"p99":5614.720990657806}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:41+00","p50":4145.5,"p95":5039.099981546402,"p99":5663.320974349976}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:42+00","p50":4001.5,"p95":5032.19996380806,"p99":5790.480005264282}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:43+00","p50":3431.5,"p95":4987.7999984025955,"p99":5153.280010223389}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:44+00","p50":4240.5,"p95":5020.599988818169,"p99":5742.800070285797}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:45+00","p50":4199.0,"p95":5178.0,"p99":5202.360065460205}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:46+00","p50":4303.75,"p95":5275.3999671936035,"p99":5853.260027885437}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:47+00","p50":3814.5,"p95":5096.049997985363,"p99":5712.480979919434}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:48+00","p50":4018.0,"p95":4971.449964702129,"p99":5537.680338859558}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:49+00","p50":3988.5,"p95":4980.199996709824,"p99":5208.360629081726}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:50+00","p50":4056.0,"p95":4693.099975824356,"p99":5280.520086288452}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:51+00","p50":4087.625,"p95":4876.699976801872,"p99":5283.570247888565}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:52+00","p50":4289.75,"p95":4996.299936771393,"p99":5420.340623855591}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:53+00","p50":4139.75,"p95":4964.499983906746,"p99":5202.500502109528}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:54+00","p50":4231.625,"p95":4886.199985027313,"p99":4973.800089836121}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:55+00","p50":4111.875,"p95":4734.249990165234,"p99":5189.850033044815}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:56+00","p50":4227.0,"p95":4820.499949932098,"p99":5050.600288391113}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:57+00","p50":4063.5,"p95":4812.499991178513,"p99":5209.760124206543}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:58+00","p50":4121.0,"p95":4805.99999332428,"p99":5157.80025100708}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:40:59+00","p50":4214.25,"p95":4781.399996519089,"p99":4894.560019493103}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:00+00","p50":4104.0,"p95":4890.399983882904,"p99":5188.650427103043}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:01+00","p50":4037.5,"p95":4660.0,"p99":4709.620055675507}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:02+00","p50":3992.25,"p95":4632.499971628189,"p99":4807.900586128235}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:03+00","p50":4008.25,"p95":4623.999964475632,"p99":5195.120017051697}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:04+00","p50":4010.0,"p95":4667.999990344048,"p99":5190.080024719238}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:05+00","p50":3946.75,"p95":4717.0,"p99":4805.860066890717}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:06+00","p50":3808.0,"p95":4477.74999409914,"p99":4544.4000062942505}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:07+00","p50":3706.5,"p95":4522.499974131584,"p99":4851.6000118255615}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:08+00","p50":3832.25,"p95":4477.7999930381775,"p99":4555.200877189636}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:09+00","p50":3853.25,"p95":4578.199990272522,"p99":5101.400077819824}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:10+00","p50":3949.25,"p95":4702.199991941452,"p99":4853.200032234192}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:11+00","p50":3889.5,"p95":4638.399987697601,"p99":4696.3400049209595}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:12+00","p50":3783.5,"p95":4550.799968719482,"p99":5084.240131378174}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:13+00","p50":3521.0,"p95":4550.749998152256,"p99":4685.800065040588}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:14+00","p50":3682.625,"p95":4508.749981224537,"p99":4571.250018358231}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:15+00","p50":3736.25,"p95":4305.899995923042,"p99":4715.040287017822}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:16+00","p50":3702.0,"p95":4349.999980926514,"p99":4917.500267028809}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:17+00","p50":3756.0,"p95":4523.149998486042,"p99":4677.53001332283}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:18+00","p50":3842.0,"p95":4572.899988055229,"p99":4668.260035037994}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:19+00","p50":3953.25,"p95":4342.0499948859215,"p99":4762.460651874542}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:20+00","p50":3989.75,"p95":4474.299987316132,"p99":4575.920078277588}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:21+00","p50":4042.125,"p95":4613.299988627434,"p99":4942.4501440525055}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:22+00","p50":3895.75,"p95":4275.0,"p99":4630.250182390213}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:23+00","p50":3898.5,"p95":4358.199971199036,"p99":4828.570205926895}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:24+00","p50":3959.0,"p95":4439.049998223782,"p99":4483.060434818268}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:25+00","p50":3957.875,"p95":4402.749991953373,"p99":4517.6002368927}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:26+00","p50":4233.0,"p95":4548.44999808073,"p99":4968.600368499756}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:27+00","p50":4095.0,"p95":4540.599988818169,"p99":4688.920366764069}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:28+00","p50":4165.0,"p95":4757.5999937057495,"p99":5160.780089378357}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:29+00","p50":4161.625,"p95":4786.0,"p99":5181.590034246445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:30+00","p50":4067.5,"p95":4961.849981248379,"p99":4995.98001909256}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:31+00","p50":4075.75,"p95":4773.899998188019,"p99":5286.980581283569}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:32+00","p50":3957.75,"p95":4773.099994421005,"p99":5290.340016365051}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:33+00","p50":3885.375,"p95":4699.749966204166,"p99":5235.1501042842865}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:34+00","p50":4028.5,"p95":4835.599996328354,"p99":4893.720010280609}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:35+00","p50":3903.25,"p95":4793.19996547699,"p99":5414.880089759827}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:36+00","p50":3753.0,"p95":4644.899936437607,"p99":5352.320072174072}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:37+00","p50":3712.25,"p95":4436.749980986118,"p99":4494.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:38+00","p50":3869.375,"p95":4477.249991595745,"p99":4585.570367097855}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:39+00","p50":3869.25,"p95":4583.2999947071075,"p99":4977.000141143799}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:40+00","p50":4007.125,"p95":4747.099996447563,"p99":4802.460491657257}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:41+00","p50":4076.5,"p95":4876.799919009209,"p99":5220.880134105682}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:42+00","p50":3983.5,"p95":4752.9999623298645,"p99":4925.800097942352}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:43+00","p50":3809.375,"p95":4687.999965906143,"p99":5200.510399580002}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:44+00","p50":4006.5,"p95":4505.2999947071075,"p99":4725.540461540222}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:45+00","p50":3866.0,"p95":4299.999996185303,"p99":4709.900535583496}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:46+00","p50":3897.5,"p95":4320.799972295761,"p99":4498.160077571869}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:47+00","p50":3807.0,"p95":4220.3999853134155,"p99":4543.800411224365}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:48+00","p50":3784.625,"p95":4066.649998128414,"p99":4304.840281486511}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:49+00","p50":3806.25,"p95":4019.0999805927277,"p99":4245.360095977783}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:50+00","p50":3883.5,"p95":4230.099980592728,"p99":4430.520318984985}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:51+00","p50":3871.25,"p95":4391.749991238117,"p99":4581.820131778717}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:52+00","p50":4030.25,"p95":4449.0,"p99":4491.7400188446045}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:53+00","p50":4027.5,"p95":4393.599977254868,"p99":4775.41007733345}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:54+00","p50":3986.0,"p95":4389.999969601631,"p99":4511.000173091888}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:55+00","p50":3933.5,"p95":4362.399989128113,"p99":4415.780015945435}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:56+00","p50":3935.625,"p95":4355.699996829033,"p99":4643.280106544495}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:57+00","p50":4046.625,"p95":4302.49998819828,"p99":4549.500361919403}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:58+00","p50":3940.75,"p95":4200.199969291687,"p99":4405.110152006149}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:41:59+00","p50":3934.75,"p95":4321.799977183342,"p99":4612.440031528473}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:00+00","p50":3780.75,"p95":3994.7499917149544,"p99":4536.200159072876}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:01+00","p50":3739.75,"p95":4024.2999980449677,"p99":4182.56007194519}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:02+00","p50":3730.875,"p95":4110.0999883413315,"p99":4257.680099487305}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:03+00","p50":3791.5,"p95":4133.949982941151,"p99":4539.600242614746}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:04+00","p50":3753.5,"p95":4172.999973297119,"p99":4379.800186157227}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:05+00","p50":3692.5,"p95":4032.999939918518,"p99":4271.66010093689}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:06+00","p50":3663.5,"p95":3980.0,"p99":4126.400009155273}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:07+00","p50":3724.0,"p95":4013.9999771118164,"p99":4067.3000259399414}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:08+00","p50":3678.5,"p95":4006.4999911785126,"p99":4243.700119972229}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:09+00","p50":3783.0,"p95":4094.349985897541,"p99":4348.050008058548}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:10+00","p50":3781.25,"p95":4203.599985599518,"p99":4445.230254888535}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:11+00","p50":3711.125,"p95":4180.499970793724,"p99":4451.000347137451}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:12+00","p50":3810.25,"p95":4103.0,"p99":4424.640263557434}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:13+00","p50":3742.375,"p95":4219.399991750717,"p99":4585.90011548996}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:14+00","p50":3683.625,"p95":4341.14997535944,"p99":4635.110031843185}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:15+00","p50":3748.0,"p95":4280.199992179871,"p99":4345.480028152466}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:16+00","p50":3678.875,"p95":4318.249987065792,"p99":4438.400106430054}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:17+00","p50":3754.5,"p95":4301.399992465973,"p99":4518.600195884705}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:18+00","p50":3843.25,"p95":4470.6999624967575,"p99":4590.800054550171}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:19+00","p50":3922.0,"p95":4495.499990224838,"p99":4562.160009384155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:20+00","p50":3925.75,"p95":4512.499990940094,"p99":4546.780015945435}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:21+00","p50":3870.5,"p95":4528.199996232986,"p99":4982.720438480377}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:22+00","p50":3831.875,"p95":4829.349979698658,"p99":4943.760129928589}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:23+00","p50":4088.875,"p95":4843.849998176098,"p99":5258.170380830765}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:24+00","p50":4065.875,"p95":4757.299961447716,"p99":5233.110051870346}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:25+00","p50":4097.0,"p95":4609.399994492531,"p99":4738.840079307556}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:26+00","p50":4058.5,"p95":4670.399996519089,"p99":4982.680197715759}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:27+00","p50":4123.625,"p95":4509.599986553192,"p99":4903.090001344681}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:28+00","p50":4219.0,"p95":4550.599996805191,"p99":4735.040088176727}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:29+00","p50":4138.0,"p95":4338.999984741211,"p99":4563.200286865234}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:30+00","p50":4128.75,"p95":4356.999983906746,"p99":4511.000210285187}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:31+00","p50":4017.25,"p95":4321.799888014793,"p99":4469.040035247803}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:32+00","p50":3900.0,"p95":4108.999961853027,"p99":4291.900077819824}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:33+00","p50":3868.75,"p95":4007.999994635582,"p99":4140.000011444092}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:34+00","p50":3816.75,"p95":4027.699987769127,"p99":4131.480019569397}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:35+00","p50":3778.875,"p95":3966.7499344944954,"p99":4163.480053901672}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:36+00","p50":3743.0,"p95":3938.799822330475,"p99":4130.240035533905}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:37+00","p50":3657.125,"p95":3839.249990403652,"p99":4001.730241060257}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:38+00","p50":3778.125,"p95":4000.1499946713448,"p99":4202.3900554180145}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:39+00","p50":3839.5,"p95":3981.5999792814255,"p99":4127.440085887909}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:40+00","p50":3823.0,"p95":3968.649998128414,"p99":4195.230166196823}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:41+00","p50":3914.5,"p95":4070.799992799759,"p99":4338.790174245834}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:42+00","p50":3831.625,"p95":4081.5499635338783,"p99":4221.280079841614}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:43+00","p50":3826.625,"p95":4127.399946928024,"p99":4255.74002122879}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:44+00","p50":3787.0,"p95":3926.699973464012,"p99":4070.910001516342}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:45+00","p50":3744.625,"p95":3926.899996161461,"p99":4053.1700813770294}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:46+00","p50":3759.375,"p95":4042.5999772548676,"p99":4364.390043973923}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:47+00","p50":3720.0,"p95":4054.749990284443,"p99":4314.150225400925}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:48+00","p50":3755.5,"p95":3939.0,"p99":4191.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:49+00","p50":3722.375,"p95":3945.499986767769,"p99":4172.40002822876}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:50+00","p50":3640.75,"p95":4047.199996471405,"p99":4151.600112915039}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:51+00","p50":3787.5,"p95":4098.899960398674,"p99":4118.980002880096}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:52+00","p50":3771.0,"p95":4104.599996089935,"p99":4302.540139198303}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:53+00","p50":3817.875,"p95":4004.4499980807304,"p99":4038.010013818741}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:54+00","p50":3852.625,"p95":4156.5999529361725,"p99":4397.3400349617}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:55+00","p50":3925.0,"p95":4101.599998354912,"p99":4376.8401737213135}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:56+00","p50":3967.5,"p95":4179.399973630905,"p99":4405.040168762207}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:57+00","p50":4075.75,"p95":4343.399998307228,"p99":4397.96001625061}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:58+00","p50":4109.625,"p95":4391.449980199337,"p99":4578.790030241013}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:42:59+00","p50":4065.125,"p95":4345.0,"p99":4426.830019712448}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:00+00","p50":3974.5,"p95":4306.999981880188,"p99":4612.100065231323}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:01+00","p50":3901.5,"p95":4157.399988412857,"p99":4375.200177669525}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:02+00","p50":3840.5,"p95":4112.599938392639,"p99":4179.96000289917}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:03+00","p50":3866.125,"p95":4085.5499945282936,"p99":4370.450124025345}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:04+00","p50":3833.25,"p95":4065.9999449253082,"p99":4168.700003147125}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:05+00","p50":3757.25,"p95":4011.5999643802643,"p99":4160.360164642334}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:06+00","p50":3751.125,"p95":3995.6999963521957,"p99":4184.510121107101}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:07+00","p50":3710.25,"p95":4005.049957692623,"p99":4248.270107984543}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:08+00","p50":3674.0,"p95":3931.999967813492,"p99":4167.00012588501}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:09+00","p50":3759.5,"p95":4058.699996113777,"p99":4311.330247163773}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:10+00","p50":3769.75,"p95":4067.9499981999397,"p99":4163.750036001205}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:11+00","p50":3842.0,"p95":4108.399949789047,"p99":4217.840066432953}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:12+00","p50":3785.25,"p95":4164.499990701675,"p99":4348.920026779175}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:13+00","p50":3796.25,"p95":4133.799991846085,"p99":4280.360136985779}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:14+00","p50":3697.5,"p95":4129.249953210354,"p99":4530.520245552063}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:15+00","p50":3682.625,"p95":4063.2499336600304,"p99":4444.860221385956}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:16+00","p50":3690.125,"p95":4032.499980568886,"p99":4159.530029535294}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:17+00","p50":3699.5,"p95":3934.149998009205,"p99":4187.380136966705}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:18+00","p50":3680.0,"p95":4010.99990940094,"p99":4220.460039138794}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:19+00","p50":3747.5,"p95":3954.1999881267548,"p99":4237.160235881805}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:20+00","p50":3758.25,"p95":3932.5999940633774,"p99":4087.6801614761353}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:21+00","p50":3666.25,"p95":3838.399983406067,"p99":4005.5600514411926}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:22+00","p50":3641.25,"p95":3825.199996471405,"p99":4071.1600818634033}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:23+00","p50":3716.75,"p95":3859.1999881267548,"p99":3946.920060157776}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:24+00","p50":3628.75,"p95":3978.2999588251114,"p99":4064.7600479125977}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:25+00","p50":3733.5,"p95":4087.499963760376,"p99":4395.700050354004}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:26+00","p50":3686.0,"p95":4249.9999833106995,"p99":4291.50000667572}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:27+00","p50":3934.0,"p95":4331.499991178513,"p99":4526.920488357544}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:28+00","p50":3920.375,"p95":4479.6499781012535,"p99":4682.180232524872}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:29+00","p50":3954.0,"p95":4509.049982726574,"p99":4593.450007677078}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:30+00","p50":3873.75,"p95":4646.699987649918,"p99":4726.720050811768}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:31+00","p50":3808.5,"p95":4651.899998188019,"p99":4729.92000579834}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:32+00","p50":3813.25,"p95":4480.0,"p99":4906.740014076233}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:33+00","p50":3804.0,"p95":4521.3999980688095,"p99":4867.44032907486}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:34+00","p50":3747.0,"p95":4475.9499943852425,"p99":4553.370013475418}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:35+00","p50":3749.5,"p95":4464.249990403652,"p99":4547.820058345795}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:36+00","p50":3744.5,"p95":4369.599987983704,"p99":4712.360557556152}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:37+00","p50":3697.0,"p95":4243.799996137619,"p99":4755.560095787048}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:38+00","p50":3636.25,"p95":4204.799974679947,"p99":4534.570183992386}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:39+00","p50":3558.375,"p95":4093.6999183893204,"p99":4683.03010725975}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:40+00","p50":3625.25,"p95":4161.899989485741,"p99":4211.840039253235}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:41+00","p50":3648.75,"p95":4127.54998165369,"p99":4431.740335941315}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:42+00","p50":3784.625,"p95":4026.249972999096,"p99":4106.850021600723}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:43+00","p50":3868.5,"p95":4207.699976801872,"p99":4324.5501391887665}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:44+00","p50":3886.5,"p95":4184.299987316132,"p99":4308.680023193359}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:45+00","p50":3980.5,"p95":4205.9499943852425,"p99":4417.29007935524}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:46+00","p50":3979.5,"p95":4232.849969804287,"p99":4364.38005399704}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:47+00","p50":3879.5,"p95":4302.199996471405,"p99":4503.680400848389}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:48+00","p50":3998.875,"p95":4324.599978685379,"p99":4368.0200028419495}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:49+00","p50":3976.125,"p95":4258.649967372417,"p99":4386.670004606247}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:50+00","p50":3909.0,"p95":4304.999983906746,"p99":4359.000221729279}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:51+00","p50":3959.75,"p95":4436.499991178513,"p99":4715.620043754578}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:52+00","p50":4015.375,"p95":4361.199980974197,"p99":4561.820058345795}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:53+00","p50":4037.0,"p95":4252.199992418289,"p99":4416.4000606536865}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:54+00","p50":4016.375,"p95":4308.499943137169,"p99":4639.660191059113}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:55+00","p50":3898.375,"p95":4271.049994170666,"p99":4451.160105705261}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:56+00","p50":3920.25,"p95":4166.99995970726,"p99":4438.800048351288}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:57+00","p50":3997.0,"p95":4215.999992012978,"p99":4301.680125236511}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:58+00","p50":4139.25,"p95":4300.999929428101,"p99":4368.240016937256}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:43:59+00","p50":4200.25,"p95":4436.399989128113,"p99":4471.940004348755}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:00+00","p50":4160.75,"p95":4467.39999628067,"p99":4568.500037193298}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:01+00","p50":4017.125,"p95":4245.949982941151,"p99":4480.100166797638}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:02+00","p50":3796.5,"p95":4132.999954223633,"p99":4437.2003173828125}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:03+00","p50":3744.5,"p95":4052.4999796152115,"p99":4215.900016307831}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:04+00","p50":3650.75,"p95":3984.149958193302,"p99":4204.530304193497}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:05+00","p50":3679.0,"p95":4052.0,"p99":4076.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:06+00","p50":3671.0,"p95":4176.699976444244,"p99":4448.080211639404}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:07+00","p50":3752.875,"p95":4152.699988484383,"p99":4261.370050668716}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:08+00","p50":3785.25,"p95":4259.299996256828,"p99":4317.930001497269}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:09+00","p50":3810.25,"p95":4284.299987316132,"p99":4591.820085525513}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:10+00","p50":3891.5,"p95":4263.249994456768,"p99":4315.800005912781}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:11+00","p50":3791.5,"p95":4199.999971628189,"p99":4499.4003047943115}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:12+00","p50":3724.5,"p95":4086.5499837994576,"p99":4278.16012096405}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:13+00","p50":3720.5,"p95":4188.399996042252,"p99":4451.720012664795}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:14+00","p50":3798.75,"p95":4160.0,"p99":4449.680313110352}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:15+00","p50":3758.5,"p95":4121.499940991402,"p99":4214.55003619194}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:16+00","p50":3726.5,"p95":4292.699993848801,"p99":4614.700024604797}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:17+00","p50":3680.625,"p95":4193.749952614307,"p99":4243.920018196106}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:18+00","p50":3650.625,"p95":4185.649980723858,"p99":4284.430674314499}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:19+00","p50":3707.375,"p95":4282.649997889996,"p99":4607.070605993271}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:20+00","p50":3735.0,"p95":4274.499998092651,"p99":4312.20002746582}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:21+00","p50":3783.5,"p95":4351.0,"p99":4673.480028629303}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:22+00","p50":3743.25,"p95":4317.1999933719635,"p99":4716.420029163361}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:23+00","p50":3798.0,"p95":4284.999959945679,"p99":4679.740251541138}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:24+00","p50":3906.75,"p95":4518.799980401993,"p99":4578.440245628357}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:25+00","p50":3973.75,"p95":4611.699987649918,"p99":5159.740052223206}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:26+00","p50":4001.875,"p95":4681.349982559681,"p99":4923.360136985779}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:27+00","p50":4065.5,"p95":4669.499952316284,"p99":4941.000076293945}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:28+00","p50":4050.75,"p95":5002.899924397469,"p99":5131.4400806427}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:29+00","p50":4181.75,"p95":4976.499991178513,"p99":5095.501376152039}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:30+00","p50":3957.0,"p95":5002.099983692169,"p99":5062.900007247925}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:31+00","p50":3836.0,"p95":4794.39999628067,"p99":5207.420510292053}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:32+00","p50":4143.125,"p95":4701.0,"p99":4835.940140247345}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:33+00","p50":3558.5,"p95":4567.599924683571,"p99":4942.120539188385}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:34+00","p50":4024.0,"p95":4501.399959087372,"p99":5219.500477313995}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:35+00","p50":3802.75,"p95":4346.199963450432,"p99":4992.960172653198}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:36+00","p50":3952.375,"p95":4278.0999883413315,"p99":4581.000310897827}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:37+00","p50":3876.0,"p95":4186.399974822998,"p99":4331.660015106201}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:38+00","p50":3803.5,"p95":4113.999984264374,"p99":4330.940104484558}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:39+00","p50":3856.0,"p95":4051.499980568886,"p99":4359.4800062179565}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:40+00","p50":3854.0,"p95":4093.7999798059464,"p99":4133.640013217926}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:41+00","p50":3937.75,"p95":4214.299958944321,"p99":4288.460017204285}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:42+00","p50":3854.75,"p95":4283.449980199337,"p99":4581.640051841736}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:43+00","p50":3793.75,"p95":4081.7999963760376,"p99":4252.460111618042}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:44+00","p50":3780.0,"p95":4195.799996137619,"p99":4334.680171489716}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:45+00","p50":3781.0,"p95":4330.249973714352,"p99":4572.710079908371}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:46+00","p50":3726.0,"p95":4346.699985980988,"p99":4621.400352478027}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:47+00","p50":3773.25,"p95":4440.799988269806,"p99":4938.8200578689575}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:48+00","p50":3778.5,"p95":4450.299987316132,"p99":4647.260126113892}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:49+00","p50":3775.125,"p95":4504.0,"p99":4557.75000166893}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:50+00","p50":3645.75,"p95":4604.149969398975,"p99":4638.8400230407715}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:51+00","p50":3739.25,"p95":4670.199998259544,"p99":4728.2000069618225}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:52+00","p50":4083.0,"p95":4841.999977827072,"p99":4918.300020694733}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:53+00","p50":3590.0,"p95":4841.999957084656,"p99":5371.100743293762}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:54+00","p50":4094.5,"p95":4889.599968671799,"p99":5076.9207282066345}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:55+00","p50":3879.0,"p95":5015.999993085861,"p99":5552.450150728226}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:56+00","p50":3771.5,"p95":4725.099994421005,"p99":4846.820004463196}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:57+00","p50":4014.75,"p95":4696.999992847443,"p99":4782.00004863739}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:58+00","p50":3628.5,"p95":4892.19998550415,"p99":5224.340265274048}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:44:59+00","p50":4038.0,"p95":5016.599987149239,"p99":5659.800778388977}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:00+00","p50":3933.5,"p95":4893.599979400635,"p99":4953.92073059082}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:01+00","p50":3928.625,"p95":4630.699971795082,"p99":5127.330149888992}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:02+00","p50":3898.625,"p95":4608.249990880489,"p99":4766.82000875473}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:03+00","p50":3845.625,"p95":4511.649994313717,"p99":5012.350128889084}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:04+00","p50":3885.0,"p95":4682.599990129471,"p99":4747.320113182068}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:05+00","p50":3917.25,"p95":4666.849983155727,"p99":5061.690498590469}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:06+00","p50":3923.0,"p95":4678.0,"p99":4852.96010351181}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:07+00","p50":3911.5,"p95":4696.299998044968,"p99":4814.380051612854}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:08+00","p50":3748.0,"p95":4838.69999063015,"p99":4989.690563440323}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:09+00","p50":3970.25,"p95":4931.34995919466,"p99":5472.730744600296}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:10+00","p50":3960.0,"p95":4826.549998104572,"p99":4872.7300045490265}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:11+00","p50":3904.0,"p95":4874.599988698959,"p99":5443.480819702148}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:12+00","p50":3903.75,"p95":5002.199993848801,"p99":5631.520014762878}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:13+00","p50":4015.75,"p95":5188.0,"p99":5664.1607275009155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:14+00","p50":4073.375,"p95":4941.799991846085,"p99":5490.6909964084625}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:15+00","p50":3649.0,"p95":4921.999972462654,"p99":5681.080988407135}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:16+00","p50":4070.5,"p95":5027.799954891205,"p99":5669.160036087036}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:17+00","p50":3418.75,"p95":4962.799998164177,"p99":5026.680048465729}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:18+00","p50":4019.25,"p95":4910.0,"p99":4970.9200320243835}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:19+00","p50":3943.25,"p95":4855.599992752075,"p99":5422.5206871032715}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:20+00","p50":4055.75,"p95":4941.099996447563,"p99":5022.091007471085}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:21+00","p50":4086.0,"p95":5042.049959123135,"p99":5105.290027856827}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:22+00","p50":3973.0,"p95":5110.0,"p99":5201.920076847076}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:23+00","p50":4227.125,"p95":4984.749987900257,"p99":5109.9000248909}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:24+00","p50":4245.125,"p95":5224.149963200092,"p99":5893.96004486084}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:25+00","p50":3982.25,"p95":5047.1999588012695,"p99":5191.100048065186}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:26+00","p50":3994.5,"p95":5035.39993584156,"p99":5585.960109233856}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:27+00","p50":4189.25,"p95":5047.199954509735,"p99":5107.370010614395}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:28+00","p50":4088.5,"p95":5042.499989628792,"p99":5553.800188064575}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:29+00","p50":4233.0,"p95":4995.1999942064285,"p99":5391.960644245148}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:30+00","p50":4147.5,"p95":4908.2999947071075,"p99":5261.840341567993}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:31+00","p50":3859.125,"p95":4871.59995508194,"p99":4913.95002245903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:32+00","p50":3759.5,"p95":4796.599976539612,"p99":4933.760025024414}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:33+00","p50":3949.0,"p95":4705.499982714653,"p99":5348.550015211105}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:34+00","p50":3850.25,"p95":4803.799992799759,"p99":5273.460509777069}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:35+00","p50":3865.0,"p95":4604.2999539375305,"p99":4808.8801345825195}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:36+00","p50":3936.75,"p95":4728.79997253418,"p99":5190.120071411133}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:37+00","p50":3966.5,"p95":4907.199949264526,"p99":5415.620100021362}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:38+00","p50":3785.375,"p95":4980.499981760979,"p99":5068.070045232773}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:39+00","p50":3850.5,"p95":4974.099957942963,"p99":5663.601249694824}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:40+00","p50":4149.625,"p95":5063.499988913536,"p99":5535.600662231445}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:41+00","p50":3454.875,"p95":5020.199985027313,"p99":5040.860002994537}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:42+00","p50":4033.5,"p95":5062.199992656708,"p99":5736.720010280609}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:43+00","p50":3699.75,"p95":4966.399994492531,"p99":5274.040292263031}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:44+00","p50":3685.75,"p95":5005.399994492531,"p99":5069.680011749268}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:45+00","p50":4023.625,"p95":4948.699996352196,"p99":5041.3400321006775}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:46+00","p50":3980.375,"p95":5041.599992513657,"p99":5823.210145235062}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:47+00","p50":3948.0,"p95":5054.749994814396,"p99":5601.400149345398}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:48+00","p50":3887.5,"p95":5115.599960446358,"p99":5197.440010547638}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:49+00","p50":3693.0,"p95":5024.9999858140945,"p99":5618.600035667419}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:50+00","p50":4084.375,"p95":4829.999985218048,"p99":4942.40007686615}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:51+00","p50":3488.5,"p95":4952.249987065792,"p99":5312.950237989426}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:52+00","p50":4081.875,"p95":4936.949995100498,"p99":5601.080020904541}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:53+00","p50":3845.5,"p95":4959.999993920326,"p99":5410.400783061981}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:54+00","p50":3802.5,"p95":4836.39999628067,"p99":4990.120071411133}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:55+00","p50":3912.75,"p95":4790.049987733364,"p99":5342.380064487457}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:56+00","p50":4332.0,"p95":4966.149986803532,"p99":5018.940025806427}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:57+00","p50":4325.25,"p95":4949.799988150597,"p99":5374.400311470032}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:58+00","p50":4422.25,"p95":5116.749991476536,"p99":5484.520321846008}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:45:59+00","p50":4360.375,"p95":5130.0,"p99":5803.270152807236}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:00+00","p50":4202.25,"p95":5041.999998211861,"p99":5117.000354766846}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:01+00","p50":4034.75,"p95":4687.149998009205,"p99":4780.670078039169}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:02+00","p50":3984.0,"p95":4630.999995350838,"p99":4678.60006570816}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:03+00","p50":3914.5,"p95":4675.999997973442,"p99":4774.00009727478}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:04+00","p50":3790.0,"p95":4738.699975132942,"p99":5158.340048313141}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:05+00","p50":3965.5,"p95":4660.399988412857,"p99":5067.440522193909}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:06+00","p50":3785.25,"p95":4886.0999982357025,"p99":5331.760194778442}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:07+00","p50":3753.0,"p95":4672.7999839782715,"p99":5105.680519104004}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:08+00","p50":4024.0,"p95":4788.999970197678,"p99":5204.750237226486}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:09+00","p50":3914.75,"p95":4925.899994850159,"p99":5513.840087890625}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:10+00","p50":4085.0,"p95":5039.899981975555,"p99":5604.660020828247}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:11+00","p50":4008.5,"p95":4883.79995059967,"p99":5307.700119972229}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:12+00","p50":4010.25,"p95":4831.299958944321,"p99":5258.920034408569}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:13+00","p50":3851.75,"p95":4888.599987149239,"p99":4936.920002937317}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:14+00","p50":3767.75,"p95":4779.199974298477,"p99":5673.640270233154}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:15+00","p50":3910.0,"p95":4752.999970316887,"p99":5306.840278625488}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:16+00","p50":3777.75,"p95":4747.649976909161,"p99":5263.730103731155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:17+00","p50":4041.5,"p95":4979.3999853134155,"p99":5353.640380382538}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:18+00","p50":3707.5,"p95":4866.199967384338,"p99":4935.980001449585}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:19+00","p50":3737.25,"p95":4896.999976396561,"p99":5451.700852870941}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:20+00","p50":4011.0,"p95":4774.199996232986,"p99":5410.080073833466}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:21+00","p50":3951.625,"p95":4831.299988627434,"p99":5388.910001516342}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:22+00","p50":3874.5,"p95":4705.199995994568,"p99":4764.100008010864}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:23+00","p50":3887.25,"p95":4551.0,"p99":5120.970035791397}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:24+00","p50":3771.375,"p95":4678.649994313717,"p99":5199.830019712448}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:25+00","p50":3846.5,"p95":4662.9999923706055,"p99":5211.900016784668}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:26+00","p50":3981.875,"p95":4667.2999967336655,"p99":4828.200574874878}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:27+00","p50":4031.25,"p95":4855.649995028973,"p99":4955.3204135894775}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:28+00","p50":3976.75,"p95":4880.149986565113,"p99":5498.4301335811615}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:29+00","p50":4089.25,"p95":4642.699970006943,"p99":4689.580040931702}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:30+00","p50":4011.75,"p95":4823.399945259094,"p99":5150.860001564026}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:31+00","p50":3825.5,"p95":4675.399951934814,"p99":5018.74001121521}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:32+00","p50":3707.75,"p95":4468.299987316132,"p99":4748.000289916992}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:33+00","p50":3752.875,"p95":4522.549994528294,"p99":4965.740499019623}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:34+00","p50":3767.0,"p95":4453.599974274635,"p99":4593.480113983154}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:35+00","p50":3751.0,"p95":4296.999971628189,"p99":4572.8003096580505}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:36+00","p50":3685.5,"p95":4467.299983263016,"p99":4862.1201457977295}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:37+00","p50":3721.0,"p95":4475.1999888420105,"p99":4997.5800104141235}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:38+00","p50":3703.875,"p95":4488.999986171722,"p99":4553.600652694702}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:39+00","p50":3919.875,"p95":4732.399993658066,"p99":5288.080664634705}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:40+00","p50":4052.5,"p95":4880.49998664856,"p99":5377.000061035156}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:41+00","p50":3980.25,"p95":4852.7499905228615,"p99":5464.970556497574}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:42+00","p50":4015.25,"p95":4922.799986362457,"p99":5016.430066823959}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:43+00","p50":4251.0,"p95":5048.499982953072,"p99":5136.430066823959}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:44+00","p50":3832.5,"p95":4944.549998342991,"p99":4983.610067605972}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:45+00","p50":4365.0,"p95":5138.499965429306,"p99":5360.080455780029}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:46+00","p50":4372.5,"p95":5257.84994238615,"p99":5720.560061454773}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:47+00","p50":4147.375,"p95":4991.299996495247,"p99":5602.840179443359}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:48+00","p50":4193.0,"p95":4899.0,"p99":5237.400275230408}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:49+00","p50":3993.375,"p95":4975.0,"p99":5416.800538063049}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:50+00","p50":3987.75,"p95":4782.999907016754,"p99":4842.820004463196}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:51+00","p50":4131.875,"p95":4762.249988734722,"p99":4817.100043773651}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:52+00","p50":4137.0,"p95":4822.599994778633,"p99":5249.400118350983}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:53+00","p50":4205.5,"p95":4683.899988651276,"p99":5132.520023345947}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:54+00","p50":4215.0,"p95":4645.299998044968,"p99":4956.620104789734}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:55+00","p50":4013.125,"p95":4531.399978399277,"p99":4740.510214567184}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:56+00","p50":4117.0,"p95":4439.799978256226,"p99":4742.960338115692}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:57+00","p50":4231.75,"p95":4491.599987030029,"p99":4894.840007781982}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:58+00","p50":4251.5,"p95":4379.899994850159,"p99":4523.96036529541}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:46:59+00","p50":4332.25,"p95":4560.499996304512,"p99":4660.050028085709}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:00+00","p50":4281.0,"p95":4697.5999937057495,"p99":4742.940041542053}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:01+00","p50":4316.75,"p95":4665.999998211861,"p99":5063.00040769577}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:02+00","p50":3927.75,"p95":4475.399992227554,"p99":4585.310020208359}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:03+00","p50":4010.375,"p95":4451.899988770485,"p99":4845.8100254535675}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:04+00","p50":3882.0,"p95":4549.249991595745,"p99":4995.880177497864}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:05+00","p50":3847.0,"p95":4547.9999858140945,"p99":5012.400085926056}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:06+00","p50":3904.0,"p95":4575.999992251396,"p99":4770.800631046295}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:07+00","p50":3991.0,"p95":4638.199986815453,"p99":5080.640666007996}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:08+00","p50":3860.625,"p95":4723.249987065792,"p99":4778.500014781952}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:09+00","p50":3946.75,"p95":4837.699995875359,"p99":5346.85017323494}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:10+00","p50":3946.375,"p95":4658.099960923195,"p99":4778.730103731155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:11+00","p50":3894.125,"p95":4678.0,"p99":5045.470640897751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:12+00","p50":3753.75,"p95":4652.0,"p99":5243.490677118301}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:13+00","p50":3766.5,"p95":4602.499990940094,"p99":4695.700021743774}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:14+00","p50":3826.875,"p95":4719.449975669384,"p99":4869.410055398941}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:15+00","p50":3911.5,"p95":4656.999947309494,"p99":4760.200022697449}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:16+00","p50":3969.25,"p95":4637.199985980988,"p99":4715.4804430007935}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:17+00","p50":3691.0,"p95":4669.999981164932,"p99":5184.5605545043945}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:18+00","p50":3888.25,"p95":4684.349982082844,"p99":5316.670078039169}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:19+00","p50":3926.625,"p95":4555.499996781349,"p99":4614.250045061111}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:20+00","p50":3900.5,"p95":4654.5999920368195,"p99":4989.4704921245575}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:21+00","p50":3829.25,"p95":4575.399989128113,"p99":5085.080139160156}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:22+00","p50":3764.25,"p95":4500.949919521809,"p99":5042.95002245903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:23+00","p50":4003.75,"p95":4714.199998259544,"p99":4829.480155944824}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:24+00","p50":3943.5,"p95":4718.44999808073,"p99":4820.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:25+00","p50":3943.75,"p95":4590.799979686737,"p99":4722.960422515869}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:26+00","p50":4012.5,"p95":4579.399969100952,"p99":4708.780498504639}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:27+00","p50":3990.625,"p95":4644.749939501286,"p99":4785.650183916092}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:28+00","p50":4197.0,"p95":4821.599993944168,"p99":4995.550466299057}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:29+00","p50":4271.625,"p95":4856.249984443188,"p99":4986.150059461594}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:30+00","p50":4240.5,"p95":4868.499994277954,"p99":4979.600021362305}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:31+00","p50":4167.0,"p95":4696.499998092651,"p99":4921.5002365112305}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:32+00","p50":4014.25,"p95":4791.549998342991,"p99":4897.240641593933}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:33+00","p50":3919.625,"p95":4768.6499781012535,"p99":5332.930113077164}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:34+00","p50":3804.25,"p95":4605.699993848801,"p99":5046.36083984375}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:35+00","p50":3856.125,"p95":4617.0,"p99":4716.0106546878815}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:36+00","p50":3633.75,"p95":4900.849998176098,"p99":4996.550021886826}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:37+00","p50":3771.75,"p95":4825.5999751091,"p99":5235.680444717407}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:38+00","p50":3894.5,"p95":4576.799996614456,"p99":4712.760639190674}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:39+00","p50":3680.5,"p95":4919.749987900257,"p99":5013.00005531311}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:40+00","p50":3986.75,"p95":5016.999991297722,"p99":5064.360917568207}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:41+00","p50":3982.25,"p95":4828.099994421005,"p99":4908.600059509277}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:42+00","p50":4106.5,"p95":5011.999996185303,"p99":5607.800140380859}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:43+00","p50":3885.0,"p95":4944.199958562851,"p99":4997.440010547638}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:44+00","p50":3655.75,"p95":4845.799988985062,"p99":5521.76092672348}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:45+00","p50":4095.0,"p95":4838.999996185303,"p99":5465.000106811523}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:46+00","p50":3844.75,"p95":4917.999992847443,"p99":5501.000147342682}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:47+00","p50":3777.75,"p95":4601.699939489365,"p99":5309.460238456726}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:48+00","p50":3610.5,"p95":4646.799968004227,"p99":4837.920568943024}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:49+00","p50":4008.5,"p95":4856.399983048439,"p99":5359.4800662994385}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:50+00","p50":3677.875,"p95":4790.249974429607,"p99":5491.950168371201}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:51+00","p50":4018.25,"p95":4684.799998164177,"p99":5172.080657958984}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:52+00","p50":3953.25,"p95":4887.499981760979,"p99":4957.190039396286}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:53+00","p50":3635.5,"p95":4779.99991607666,"p99":5390.600051879883}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:54+00","p50":3848.5,"p95":4680.999953508377,"p99":5089.000011444092}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:55+00","p50":3863.0,"p95":4749.199986815453,"p99":5226.280126571655}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:56+00","p50":3976.5,"p95":4675.699994564056,"p99":4871.860010147095}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:57+00","p50":3838.625,"p95":4636.1499591469765,"p99":5178.010001420975}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:58+00","p50":3961.5,"p95":4714.699985980988,"p99":4802.280006408691}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:47:59+00","p50":3937.625,"p95":4543.149969398975,"p99":5027.990001440048}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:00+00","p50":3845.375,"p95":4488.599992513657,"p99":4716.640221595764}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:01+00","p50":3789.0,"p95":4547.399959445,"p99":4731.040205478668}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:02+00","p50":3853.75,"p95":4475.99999153614,"p99":4553.160544395447}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:03+00","p50":3910.5,"p95":4523.599988698959,"p99":4574.08003616333}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:04+00","p50":3816.0,"p95":4465.999979257584,"p99":4728.0}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:05+00","p50":3842.125,"p95":4381.049979388714,"p99":4449.170001268387}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:06+00","p50":3844.125,"p95":4447.699987053871,"p99":4545.700051784515}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:07+00","p50":3689.0,"p95":4089.0,"p99":4228.900047302246}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:08+00","p50":3765.125,"p95":4172.699989199638,"p99":4291.580060482025}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:09+00","p50":3846.0,"p95":4273.99999153614,"p99":4352.320039272308}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:10+00","p50":3864.125,"p95":4287.699966311455,"p99":4594.960107803345}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:11+00","p50":3947.75,"p95":4371.349929869175,"p99":4632.950068235397}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:12+00","p50":3959.125,"p95":4401.0,"p99":4733.7004227638245}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:13+00","p50":3854.125,"p95":4362.999992609024,"p99":4401.950001478195}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:14+00","p50":3915.0,"p95":4156.999984741211,"p99":4228.700035095215}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:15+00","p50":3838.375,"p95":4121.0,"p99":4344.410055398941}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:16+00","p50":3733.125,"p95":4052.7499712109566,"p99":4235.920196533203}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:17+00","p50":3699.0,"p95":4024.9999655485153,"p99":4406.800107002258}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:18+00","p50":3728.0,"p95":3921.799973964691,"p99":4143.58008480072}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:19+00","p50":3721.375,"p95":3900.699933409691,"p99":4164.180148601532}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:20+00","p50":3787.75,"p95":4051.8999869823456,"p99":4157.460087776184}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:21+00","p50":3976.5,"p95":4309.399989485741,"p99":4653.520027637482}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:22+00","p50":4091.0,"p95":4561.399993896484,"p99":4797.100128173828}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:23+00","p50":4439.5,"p95":4735.599990129471,"p99":4905.840075016022}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:24+00","p50":4348.5,"p95":4813.0,"p99":5065.400014877319}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:25+00","p50":4171.75,"p95":4925.999949932098,"p99":5134.000022888184}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:26+00","p50":4164.875,"p95":4903.249924361706,"p99":5368.080016136169}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:27+00","p50":4142.25,"p95":4933.499996781349,"p99":5411.300028324127}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:28+00","p50":4097.375,"p95":4784.049986064434,"p99":5398.181028842926}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:29+00","p50":4048.125,"p95":4835.799992799759,"p99":4875.880017280579}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:30+00","p50":3917.0,"p95":4785.699996352196,"p99":4849.700014591217}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:31+00","p50":4047.25,"p95":4893.0,"p99":5377.790004491806}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:32+00","p50":3781.5,"p95":4729.399969100952,"p99":5572.800796508789}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:33+00","p50":3981.5,"p95":5123.999984264374,"p99":5255.400037765503}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:34+00","p50":3972.5,"p95":5113.199991226196,"p99":5525.920722961426}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:35+00","p50":3800.5,"p95":4844.899964332581,"p99":5510.280120849609}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:36+00","p50":3889.875,"p95":4823.699996352196,"p99":5378.8901998996735}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:37+00","p50":3935.75,"p95":4917.499975800514,"p99":5066.000110626221}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:38+00","p50":3921.25,"p95":4793.149979412556,"p99":4931.710070371628}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:39+00","p50":3715.0,"p95":4943.399994492531,"p99":4989.48001909256}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:40+00","p50":4053.5,"p95":5012.0,"p99":5100.420042037964}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:41+00","p50":4078.5,"p95":5131.599974274635,"p99":5902.8802485466}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:42+00","p50":3590.75,"p95":5073.799988150597,"p99":5685.760063648224}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:43+00","p50":4152.5,"p95":5012.399996519089,"p99":5039.920136451721}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:44+00","p50":4073.75,"p95":5238.699987769127,"p99":5795.9409556388855}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:45+00","p50":3704.75,"p95":5178.699970006943,"p99":6039.960067749023}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:46+00","p50":4157.75,"p95":5117.399975299835,"p99":5838.460173606873}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:47+00","p50":4235.75,"p95":5206.099989056587,"p99":5855.8307864665985}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:48+00","p50":3752.0,"p95":5350.399987220764,"p99":5547.840797424316}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:49+00","p50":4351.5,"p95":5367.799958229065,"p99":6352.281228065491}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:50+00","p50":3552.625,"p95":5452.399985790253,"p99":6108.180025577545}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:51+00","p50":4613.5,"p95":5567.899995565414,"p99":6266.980086326599}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:52+00","p50":3763.0,"p95":5803.499938249588,"p99":6406.1003885269165}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:53+00","p50":4708.5,"p95":5896.849981248379,"p99":6606.970233201981}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:54+00","p50":4324.0,"p95":5743.9999767541885,"p99":6777.000341892242}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:55+00","p50":4356.0,"p95":5725.299974799156,"p99":5805.880017280579}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:56+00","p50":4050.0,"p95":5631.999966144562,"p99":6712.481091499329}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:57+00","p50":4302.0,"p95":5450.399964332581,"p99":6319.560264587402}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:58+00","p50":4265.625,"p95":5413.49998152256,"p99":5566.150054693222}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:48:59+00","p50":4419.0,"p95":5370.699993848801,"p99":6096.24009513855}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:00+00","p50":4469.0,"p95":5208.999988555908,"p99":5676.900352478027}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:01+00","p50":3535.375,"p95":5112.849994599819,"p99":5195.760034561157}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:02+00","p50":3731.625,"p95":4856.449981868267,"p99":5018.050008058548}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:03+00","p50":3798.0,"p95":4608.399978399277,"p99":4650.7000432014465}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:04+00","p50":3722.125,"p95":4758.899988770485,"p99":4831.580008983612}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:05+00","p50":3883.0,"p95":4789.499968886375,"p99":4932.500815868378}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:06+00","p50":4029.875,"p95":4700.049961268902,"p99":5354.700048923492}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:07+00","p50":4076.375,"p95":4753.249990642071,"p99":5254.760646820068}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:08+00","p50":3982.5,"p95":5056.399987816811,"p99":5144.040523529053}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:09+00","p50":4046.25,"p95":4961.5999965667725,"p99":5734.9200439453125}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:10+00","p50":3933.875,"p95":5035.549998104572,"p99":5068.470025777817}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:11+00","p50":4021.0,"p95":4987.749991238117,"p99":5141.031122922897}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:12+00","p50":4022.5,"p95":5127.999992132187,"p99":5154.850001573563}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:13+00","p50":3656.75,"p95":5117.999974608421,"p99":5244.0805768966675}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:14+00","p50":3483.75,"p95":5021.499974131584,"p99":5719.050028085709}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:15+00","p50":3816.75,"p95":5130.899994134903,"p99":5774.160009384155}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:16+00","p50":4107.125,"p95":5035.04999345541,"p99":5470.0609040260315}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:17+00","p50":3316.5,"p95":4813.999894142151,"p99":5509.560039520264}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:18+00","p50":3940.5,"p95":4859.599988698959,"p99":4954.320031642914}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:19+00","p50":3984.25,"p95":4746.0,"p99":4823.240008354187}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:20+00","p50":3964.375,"p95":4762.749990999699,"p99":4955.170119524002}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:21+00","p50":4061.875,"p95":4974.99996638298,"p99":5462.570098161697}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:22+00","p50":4056.25,"p95":5108.3999853134155,"p99":5777.840299606323}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:23+00","p50":4049.75,"p95":5025.999962568283,"p99":5077.650007486343}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:24+00","p50":3997.625,"p95":4958.749998152256,"p99":5020.700038433075}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:25+00","p50":3962.875,"p95":5018.799971580505,"p99":5097.280181884766}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:26+00","p50":3690.25,"p95":4986.699989914894,"p99":5720.040344238281}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:27+00","p50":4054.875,"p95":5152.749991476536,"p99":5858.700013637543}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:28+00","p50":4337.0,"p95":5150.999972462654,"p99":5871.720157146454}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:29+00","p50":4380.0,"p95":5173.899988770485,"p99":5825.11093878746}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:30+00","p50":4315.125,"p95":5116.699996352196,"p99":5197.430027723312}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:31+00","p50":3974.5,"p95":5118.2999947071075,"p99":5145.040002822876}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:32+00","p50":3886.5,"p95":5066.199996471405,"p99":5139.140009880066}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:33+00","p50":4055.5,"p95":5002.7999522686005,"p99":5606.080547809601}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:34+00","p50":3884.25,"p95":4879.249959170818,"p99":5264.121599197388}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:35+00","p50":4153.0,"p95":4955.399996042252,"p99":5067.3200759887695}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:36+00","p50":4105.375,"p95":4800.249990642071,"p99":5508.010085344315}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:37+00","p50":3917.625,"p95":4675.849998176098,"p99":4755.5300714969635}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:38+00","p50":3865.5,"p95":4642.59998190403,"p99":5176.760063171387}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:39+00","p50":3965.0,"p95":4704.549983799458,"p99":4793.83002448082}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:40+00","p50":4010.25,"p95":4676.799966931343,"p99":5088.7600264549255}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:41+00","p50":4063.75,"p95":4831.049998223782,"p99":4899.600511550903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:42+00","p50":3985.625,"p95":4835.349998295307,"p99":5385.200081825256}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:43+00","p50":4077.75,"p95":5004.99999666214,"p99":5389.100094795227}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:44+00","p50":4100.75,"p95":4879.149998009205,"p99":5017.8800573349}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:45+00","p50":3894.75,"p95":4927.499990701675,"p99":5500.200044631958}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:46+00","p50":3978.875,"p95":4839.549993813038,"p99":4951.960079193115}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:47+00","p50":4007.375,"p95":4703.849981248379,"p99":5110.6101677417755}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:48+00","p50":3698.0,"p95":4717.49998486042,"p99":4775.75003027916}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:49+00","p50":4185.0,"p95":4822.199961185455,"p99":4976.060004234314}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:50+00","p50":3949.0,"p95":4817.799949169159,"p99":5228.300164222717}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:51+00","p50":4259.5,"p95":4907.599983692169,"p99":5091.080111980438}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:52+00","p50":4462.0,"p95":5324.499985337257,"p99":5431.530045747757}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:53+00","p50":4415.75,"p95":5294.249991595745,"p99":6003.970716714859}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:54+00","p50":4396.875,"p95":5010.749998152256,"p99":5102.000029563904}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:55+00","p50":4352.75,"p95":5009.199992656708,"p99":5462.520054340363}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:56+00","p50":4087.75,"p95":5137.0,"p99":5250.3805565834045}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:57+00","p50":4153.0,"p95":4888.849987447262,"p99":5118.300569057465}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:58+00","p50":4421.5,"p95":5287.999987125397,"p99":5405.200087547302}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:49:59+00","p50":4552.0,"p95":5317.999981760979,"p99":5914.600027561188}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:00+00","p50":4620.0,"p95":5413.199975967407,"p99":6032.080024719238}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:01+00","p50":4531.25,"p95":5441.599977254868,"p99":5525.470025777817}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:02+00","p50":3941.5,"p95":5481.899972438812,"p99":5695.100667953491}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:03+00","p50":4083.5,"p95":5444.44998639822,"p99":5601.46009016037}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:04+00","p50":4044.75,"p95":5062.999982357025,"p99":5162.56166267395}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:05+00","p50":3774.75,"p95":5030.399959087372,"p99":5139.100022315979}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:06+00","p50":3938.75,"p95":5171.499990701675,"p99":5802.880077362061}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:07+00","p50":3785.125,"p95":5188.849925220013,"p99":5992.630990743637}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:08+00","p50":4263.25,"p95":5200.79998588562,"p99":5454.480457305908}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:09+00","p50":3542.5,"p95":5367.798892021179,"p99":6125.560956954956}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:10+00","p50":4299.25,"p95":5218.1999335289,"p99":5888.920112609863}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:11+00","p50":4124.625,"p95":4942.749971926212,"p99":5694.200059890747}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:12+00","p50":3990.0,"p95":4946.199948549271,"p99":5632.6000237464905}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:13+00","p50":3550.75,"p95":4925.999998211861,"p99":5006.000798225403}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:14+00","p50":3927.25,"p95":4744.3999927043915,"p99":4792.670016050339}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:15+00","p50":3998.75,"p95":4772.749978363514,"p99":4943.0500519275665}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:16+00","p50":3674.5,"p95":4663.399988412857,"p99":4782.600069522858}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:17+00","p50":3838.5,"p95":4741.949971020222,"p99":5284.120021820068}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:18+00","p50":3981.0,"p95":4876.74999243021,"p99":5092.850599527359}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:19+00","p50":4020.75,"p95":4878.499941706657,"p99":5418.230045080185}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:20+00","p50":4114.0,"p95":4760.599992752075,"p99":4840.900007247925}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:21+00","p50":4181.5,"p95":4965.599986076355,"p99":5329.760165691376}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:22+00","p50":3918.5,"p95":4996.249970138073,"p99":5113.070046186447}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:23+00","p50":3850.125,"p95":4570.049998223782,"p99":4905.210029840469}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:24+00","p50":4024.25,"p95":4666.099970817566,"p99":4937.440170288086}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:25+00","p50":3834.0,"p95":4613.999962449074,"p99":4651.0005078315735}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:26+00","p50":3992.0,"p95":4516.499972105026,"p99":4873.880151748657}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:27+00","p50":4066.5,"p95":4501.199955582619,"p99":4605.960240840912}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:28+00","p50":4078.0,"p95":4627.499981641769,"p99":4976.600141525269}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:29+00","p50":4093.25,"p95":4603.099996447563,"p99":4704.100298404694}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:30+00","p50":4114.5,"p95":4716.199901819229,"p99":5103.040322303772}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:31+00","p50":4128.0,"p95":4522.599976539612,"p99":4793.340029716492}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:32+00","p50":4034.75,"p95":4357.44998639822,"p99":4397.830013990402}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:33+00","p50":3753.25,"p95":4319.899994850159,"p99":4519.960296630859}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:34+00","p50":3808.75,"p95":4263.899980068207,"p99":4731.880443572998}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:35+00","p50":3878.5,"p95":4171.199998021126,"p99":4273.720052242279}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:36+00","p50":3929.375,"p95":4101.099990487099,"p99":4189.4501078128815}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:37+00","p50":3921.25,"p95":4186.799991846085,"p99":4259.27002120018}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:38+00","p50":3975.75,"p95":4206.999983787537,"p99":4574.880054473877}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:39+00","p50":4036.25,"p95":4262.5998878479,"p99":4435.100098133087}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:40+00","p50":4037.25,"p95":4226.599996328354,"p99":4292.840005874634}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:41+00","p50":3979.5,"p95":4269.999988555908,"p99":4307.700004577637}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:42+00","p50":3931.75,"p95":4249.249991118908,"p99":4346.3900554180145}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:43+00","p50":4015.75,"p95":4351.599959373474,"p99":4700.600128650665}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:44+00","p50":3926.0,"p95":4197.9999878406525,"p99":4605.800552845001}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:45+00","p50":3853.0,"p95":4189.599969863892,"p99":4237.040018081665}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:46+00","p50":3819.625,"p95":4247.1499653458595,"p99":4311.610018968582}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:47+00","p50":3791.5,"p95":4272.399988412857,"p99":4631.040089607239}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:48+00","p50":3859.25,"p95":4179.8999761343,"p99":4376.66018819809}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:49+00","p50":3949.75,"p95":4338.2999947071075,"p99":4408.140009880066}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:50+00","p50":3957.125,"p95":4449.249991118908,"p99":4622.230032682419}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:51+00","p50":3976.5,"p95":4506.999996185303,"p99":4796.500343322754}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:52+00","p50":3955.875,"p95":4406.699957251549,"p99":4560.570172548294}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:53+00","p50":3763.5,"p95":4130.699998140335,"p99":4399.660206794739}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:54+00","p50":3897.0,"p95":4351.299985408783,"p99":4421.700006484985}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:55+00","p50":3944.0,"p95":4299.799992799759,"p99":4457.850165605545}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:56+00","p50":4126.0,"p95":4395.049996316433,"p99":4425.580013751984}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:57+00","p50":4391.75,"p95":4651.949998438358,"p99":4749.090013742447}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:58+00","p50":4655.5,"p95":5036.299990773201,"p99":5080.470008611679}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:50:59+00","p50":4580.25,"p95":4921.49999499321,"p99":5003.900385856628}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:00+00","p50":4689.5,"p95":4967.699970602989,"p99":5040.73002743721}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:01+00","p50":4440.875,"p95":4876.299976468086,"p99":5202.870057821274}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:02+00","p50":4328.75,"p95":4878.49999165535,"p99":5125.7003698349}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:03+00","p50":4193.5,"p95":4722.699998140335,"p99":4789.980025291443}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:04+00","p50":4091.375,"p95":4659.649987399578,"p99":4910.4602217674255}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:05+00","p50":4006.5,"p95":4566.599991798401,"p99":4859.160364151001}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:06+00","p50":3818.75,"p95":4469.599962234497,"p99":4923.620105743408}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:07+00","p50":3892.25,"p95":4510.599984765053,"p99":4694.920167922974}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:08+00","p50":3888.375,"p95":4606.199954509735,"p99":4704.660039424896}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:09+00","p50":3960.25,"p95":4550.999959945679,"p99":5499.000093460083}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:10+00","p50":4049.625,"p95":4773.499911189079,"p99":5207.880125045776}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:11+00","p50":3972.0,"p95":4834.0,"p99":4951.750115156174}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:12+00","p50":3865.75,"p95":4558.049997985363,"p99":4601.110049962997}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:13+00","p50":3847.0,"p95":4593.399970412254,"p99":4963.080037593842}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:14+00","p50":3855.0,"p95":4726.399954795837,"p99":5388.920980930328}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:15+00","p50":3831.0,"p95":4732.149963200092,"p99":4829.0600028038025}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:16+00","p50":3910.0,"p95":4692.599982619286,"p99":5409.320098876953}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:17+00","p50":4027.75,"p95":4783.299935221672,"p99":4923.9304077625275}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:18+00","p50":3877.5,"p95":5004.349979937077,"p99":5629.140820026398}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:19+00","p50":3709.5,"p95":4995.799966096878,"p99":5614.240033149719}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:20+00","p50":3986.75,"p95":4975.2999893426895,"p99":5510.900269985199}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:21+00","p50":3878.0,"p95":5252.699990987778,"p99":5755.1001081466675}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:22+00","p50":4126.0,"p95":5518.149989902973,"p99":5594.320009231567}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:23+00","p50":4444.25,"p95":5661.249963700771,"p99":6313.150087118149}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:24+00","p50":4544.875,"p95":5474.299996256828,"p99":6253.121173858643}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:25+00","p50":4501.0,"p95":5457.399930477142,"p99":6005.640081882477}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:26+00","p50":4235.625,"p95":5376.999968767166,"p99":5461.830196142197}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:27+00","p50":4000.0,"p95":5107.9999969005585,"p99":5332.2007575035095}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:28+00","p50":4163.5,"p95":5209.4999849796295,"p99":5357.800891876221}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:29+00","p50":4370.875,"p95":5248.099973797798,"p99":5300.320035934448}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:30+00","p50":4376.125,"p95":5258.649987399578,"p99":5367.530067682266}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:31+00","p50":4107.0,"p95":5051.349987566471,"p99":5340.290467500687}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:32+00","p50":3944.5,"p95":5053.999990582466,"p99":5598.320785045624}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:33+00","p50":3968.0,"p95":4784.999981164932,"p99":5169.40051984787}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:34+00","p50":3915.5,"p95":4720.599998354912,"p99":4812.120001316071}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:35+00","p50":3815.5,"p95":4759.0,"p99":5275.0000829696655}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:36+00","p50":3857.5,"p95":4562.999987483025,"p99":4645.000776767731}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:37+00","p50":3863.25,"p95":4678.999992847443,"p99":4773.000557899475}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:38+00","p50":3880.5,"p95":4635.499996066093,"p99":5105.700789928436}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:39+00","p50":3858.5,"p95":4527.999996423721,"p99":5177.000020027161}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:40+00","p50":3862.875,"p95":4682.649947106838,"p99":4752.82000875473}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:41+00","p50":3857.75,"p95":4825.749998152256,"p99":5335.30007982254}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:42+00","p50":3899.0,"p95":4675.999996185303,"p99":5102.9006271362305}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:43+00","p50":3961.875,"p95":4815.249972999096,"p99":4900.630053281784}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:44+00","p50":4007.0,"p95":4787.999993085861,"p99":5424.400342941284}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:45+00","p50":4095.25,"p95":4888.2499913573265,"p99":5008.200475692749}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:46+00","p50":4017.75,"p95":4987.299987316132,"p99":5047.700021743774}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:47+00","p50":3884.0,"p95":5094.799949645996,"p99":5614.4401779174805}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:48+00","p50":3721.5,"p95":4799.3999853134155,"p99":4852.48001909256}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:49+00","p50":4024.375,"p95":4852.649987399578,"p99":5863.951159238815}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:50+00","p50":4041.5,"p95":4807.199996709824,"p99":4887.560017108917}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:51+00","p50":3973.0,"p95":5073.999977827072,"p99":5619.600780487061}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:52+00","p50":4012.5,"p95":5036.199993133545,"p99":5663.140232086182}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:53+00","p50":4069.25,"p95":5071.749971210957,"p99":5540.850560426712}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:54+00","p50":3743.5,"p95":5066.599977970123,"p99":5171.880004405975}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:55+00","p50":4135.125,"p95":5263.949985086918,"p99":5764.020241260529}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:56+00","p50":3959.125,"p95":5169.0,"p99":6159.080020904541}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:57+00","p50":4038.5,"p95":5429.199996948242,"p99":5567.1600341796875}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:58+00","p50":4174.5,"p95":5559.349998056889,"p99":5635.790026426315}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:51:59+00","p50":4492.25,"p95":5449.949982941151,"p99":5673.230080366135}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:00+00","p50":4448.25,"p95":5403.19997882843,"p99":5484.680965423584}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:01+00","p50":3636.25,"p95":5473.499975681305,"p99":5510.700006484985}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:02+00","p50":4389.0,"p95":5455.899990916252,"p99":5506.880067825317}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:03+00","p50":4041.75,"p95":5368.899996161461,"p99":5486.250038385391}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:04+00","p50":4011.75,"p95":5469.499991178513,"p99":6215.080005645752}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:05+00","p50":3851.0,"p95":5509.2999893426895,"p99":5590.410058259964}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:06+00","p50":4245.0,"p95":5393.0,"p99":6357.080063343048}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:07+00","p50":3658.25,"p95":5396.59992814064,"p99":6220.810048341751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:08+00","p50":4444.0,"p95":5446.499974966049,"p99":6207.100028038025}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:09+00","p50":4124.0,"p95":5495.0999982357025,"p99":5546.381085395813}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:10+00","p50":4273.0,"p95":5513.699996352196,"p99":6367.871124982834}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:11+00","p50":3940.5,"p95":5459.199996232986,"p99":5499.400030136108}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:12+00","p50":4283.75,"p95":5533.349998295307,"p99":6357.9900777339935}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:13+00","p50":4227.5,"p95":5427.399983048439,"p99":5478.600045204163}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:14+00","p50":4335.25,"p95":5298.799998164177,"p99":6209.640270233154}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:15+00","p50":3459.375,"p95":5317.399986743927,"p99":5517.390595197678}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:16+00","p50":4412.125,"p95":5363.499995827675,"p99":6060.750021696091}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:17+00","p50":3501.625,"p95":5382.9499943852425,"p99":5437.530031442642}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:18+00","p50":3581.0,"p95":5290.599980831146,"p99":6240.360026836395}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:19+00","p50":3700.125,"p95":5448.849983155727,"p99":6199.110489606857}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:20+00","p50":4179.5,"p95":5374.89997279644,"p99":5683.940096378326}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:21+00","p50":4351.0,"p95":5307.199996471405,"p99":5414.280725479126}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:22+00","p50":3493.5,"p95":5287.499992132187,"p99":6013.140154838562}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:23+00","p50":4378.25,"p95":5418.899986982346,"p99":6071.7200565338135}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:24+00","p50":3898.25,"p95":5299.899987339973,"p99":5378.530102968216}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:25+00","p50":4173.125,"p95":5053.5499702095985,"p99":5678.960465431213}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:26+00","p50":4219.875,"p95":5128.899934411049,"p99":5314.09076333046}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:27+00","p50":4135.75,"p95":5182.949998438358,"p99":6117.680214881897}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:28+00","p50":4235.125,"p95":5169.649995028973,"p99":5196.110001325607}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:29+00","p50":4398.0,"p95":5078.999997973442,"p99":5213.800025939941}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:30+00","p50":3866.875,"p95":5107.949985563755,"p99":5237.390011548996}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:31+00","p50":3903.0,"p95":5077.099980592728,"p99":5171.140009880066}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:32+00","p50":3707.0,"p95":4767.599992752075,"p99":4816.92000579834}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:33+00","p50":3868.125,"p95":4621.649987399578,"p99":5288.380233287811}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:34+00","p50":3829.75,"p95":4779.699984550476,"p99":4903.720497131348}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:35+00","p50":3989.0,"p95":4957.799986481667,"p99":5030.640004634857}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:36+00","p50":4051.0,"p95":5038.799984931946,"p99":5078.200015068054}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:37+00","p50":3805.0,"p95":4799.899994134903,"p99":5318.740796089172}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:38+00","p50":4187.0,"p95":5058.399969577789,"p99":5136.700005531311}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:39+00","p50":3719.125,"p95":5113.149998247623,"p99":5245.720454216003}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:40+00","p50":3965.5,"p95":5073.749998152256,"p99":5623.450045824051}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:41+00","p50":4282.0,"p95":5156.7999584674835,"p99":5400.520539283752}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:42+00","p50":4178.625,"p95":5234.649947106838,"p99":5727.170089006424}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:43+00","p50":4125.25,"p95":5278.0,"p99":5396.0200028419495}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:44+00","p50":4038.5,"p95":4929.4999895095825,"p99":5012.480003356934}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:45+00","p50":3798.5,"p95":4762.599995613098,"p99":5320.480136871338}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:46+00","p50":3561.5,"p95":4460.499988913536,"p99":4504.700008869171}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:47+00","p50":3395.875,"p95":4627.199947595596,"p99":5285.95002245903}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:48+00","p50":3723.5,"p95":4560.599996328354,"p99":5155.560089588165}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:49+00","p50":3825.5,"p95":4595.099987983704,"p99":4696.700611114502}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:50+00","p50":3853.75,"p95":4749.099975824356,"p99":5265.880077362061}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:51+00","p50":4125.375,"p95":4939.949995100498,"p99":5338.49022603035}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:52+00","p50":4187.0,"p95":5015.499986052513,"p99":5230.65056347847}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:53+00","p50":4295.125,"p95":5137.549986898899,"p99":5688.230016469955}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:54+00","p50":4167.5,"p95":5155.249973714352,"p99":5197.710079908371}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:55+00","p50":4183.25,"p95":5217.199989557266,"p99":6018.400710105896}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:56+00","p50":4345.75,"p95":5371.499934911728,"p99":5511.700196266174}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:57+00","p50":4380.0,"p95":5370.999983787537,"p99":5586.400596618652}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:58+00","p50":4133.5,"p95":5190.299998283386,"p99":5230.720016479492}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:52:59+00","p50":4226.5,"p95":5309.0,"p99":5392.520046710968}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:00+00","p50":3786.0,"p95":5222.999985694885,"p99":5301.000605106354}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:01+00","p50":4210.625,"p95":5118.0,"p99":5748.8501789569855}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:02+00","p50":3959.5,"p95":5000.599992275238,"p99":5767.160203933716}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:03+00","p50":3977.875,"p95":4813.999992609024,"p99":5541.55019068718}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:04+00","p50":3960.875,"p95":4768.14998203516,"p99":5417.860159397125}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:05+00","p50":3864.75,"p95":4858.0,"p99":5378.900590419769}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:06+00","p50":4019.25,"p95":4945.39998626709,"p99":5037.20085144043}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:07+00","p50":4257.0,"p95":5059.0,"p99":5780.670110464096}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:08+00","p50":4063.0,"p95":5227.099954366684,"p99":5468.960657119751}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:09+00","p50":4368.25,"p95":5421.999983906746,"p99":5451.000044345856}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:10+00","p50":4585.0,"p95":5542.249978005886,"p99":6123.150052785873}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:11+00","p50":4211.625,"p95":5601.349971830845,"p99":5696.550801992416}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:12+00","p50":4645.0,"p95":5611.74999409914,"p99":5650.400037765503}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:13+00","p50":4313.0,"p95":5514.599963307381,"p99":6098.440792560577}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:14+00","p50":3934.25,"p95":5213.74999409914,"p99":5304.200050354004}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:15+00","p50":3534.0,"p95":4761.899993419647,"p99":5094.940542221069}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:16+00","p50":3775.0,"p95":4544.499980568886,"p99":4609.350007772446}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:17+00","p50":3327.5,"p95":4612.599998116493,"p99":4654.480028629303}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:18+00","p50":3788.0,"p95":4695.999977827072,"p99":5318.050855875015}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:19+00","p50":3846.0,"p95":4673.499979019165,"p99":5388.600234985352}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:20+00","p50":3620.0,"p95":4896.299824357033,"p99":5345.79011797905}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:21+00","p50":3702.0,"p95":5090.499981760979,"p99":5232.650065660477}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:22+00","p50":4200.125,"p95":5117.299996495247,"p99":5821.840179443359}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:23+00","p50":3705.875,"p95":5136.199978113174,"p99":5828.130042314529}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:24+00","p50":3856.125,"p95":5243.899989485741,"p99":5334.600028038025}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:25+00","p50":4259.75,"p95":5340.5499702095985,"p99":6018.630169630051}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:26+00","p50":3982.25,"p95":5388.499968290329,"p99":6304.40012550354}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:27+00","p50":4378.875,"p95":5405.1499953866005,"p99":5518.470008611679}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:28+00","p50":4057.0,"p95":5572.999969959259,"p99":6300.500247001648}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:29+00","p50":4545.75,"p95":5587.99997317791,"p99":6216.000181674957}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:30+00","p50":3978.75,"p95":5647.59997177124,"p99":5738.621031761169}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:31+00","p50":4564.875,"p95":5730.349982559681,"p99":5813.210016489029}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:32+00","p50":3822.875,"p95":5703.0,"p99":6574.631205320358}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:33+00","p50":4423.75,"p95":5526.999990105629,"p99":5605.120028495789}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:34+00","p50":4326.625,"p95":5317.749998152256,"p99":5483.15011382103}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:35+00","p50":3370.0,"p95":5085.799996376038,"p99":5545.860155105591}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:36+00","p50":3865.75,"p95":5261.999992251396,"p99":6170.000024795532}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:37+00","p50":4163.0,"p95":5247.499969244003,"p99":5970.301205635071}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:38+00","p50":3906.125,"p95":5078.199967384338,"p99":5118.370004892349}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:39+00","p50":4096.75,"p95":4982.0,"p99":5287.8500854969025}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:40+00","p50":3459.625,"p95":4948.44998639822,"p99":5645.410066843033}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:41+00","p50":3844.75,"p95":4786.999989628792,"p99":5644.120144367218}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:42+00","p50":3398.5,"p95":4709.299996256828,"p99":5205.240700721741}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:43+00","p50":3936.25,"p95":4819.099980592728,"p99":4870.921052932739}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:44+00","p50":3867.0,"p95":4820.1999888420105,"p99":5437.800029754639}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:45+00","p50":3939.5,"p95":4784.499981760979,"p99":4826.760011672974}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:46+00","p50":3634.0,"p95":4930.1999834775925,"p99":5079.400058746338}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:47+00","p50":4172.0,"p95":5039.449984014034,"p99":5147.150731801987}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:48+00","p50":4272.25,"p95":5115.799984335899,"p99":5202.2000069618225}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:49+00","p50":4307.125,"p95":5260.749991476536,"p99":6024.360065460205}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:50+00","p50":3932.0,"p95":5214.099996209145,"p99":6070.580057621002}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:51+00","p50":3944.5,"p95":5019.1999888420105,"p99":5216.380188941956}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:52+00","p50":4276.5,"p95":5185.999989151955,"p99":5275.800054550171}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:53+00","p50":3894.0,"p95":5241.999979972839,"p99":5427.900839805603}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:54+00","p50":4183.0,"p95":5332.999984741211,"p99":5403.000030517578}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:55+00","p50":3955.75,"p95":5185.44999474287,"p99":5974.470068693161}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:56+00","p50":4319.5,"p95":5500.499991893768,"p99":6038.8402671813965}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:57+00","p50":3962.0,"p95":5619.999971628189,"p99":6394.440254211426}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:58+00","p50":4223.0,"p95":5598.099997758865,"p99":5748.180069923401}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:53:59+00","p50":3481.3333333333335,"p95":5079.04994314909,"p99":5247.540110111237}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:54:00+00","p50":3547.777777777778,"p95":4354.799990415573,"p99":4868.0605635643005}, + {"metric_name":"oidc_session_duration","timestamp":"2025-02-25 14:54:01+00","p50":2788.0,"p95":2858.34999614954,"p99":2865.0}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:23:59+00","p50":318.257213875,"p95":514.5172733277097,"p99":531.5560519789628}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:00+00","p50":615.2308725,"p95":744.9976568722952,"p99":779.5591113678698}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:01+00","p50":790.838948875,"p95":875.6845375629654,"p99":893.9481238409545}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:02+00","p50":906.75765375,"p95":1010.2756786086649,"p99":1021.1621610457669}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:03+00","p50":1031.79282525,"p95":1290.6646419817835,"p99":1301.9739752688756}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:04+00","p50":1148.421545875,"p95":1355.5298260700195,"p99":1369.5902623623933}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:05+00","p50":1302.860049,"p95":1373.4843539683,"p99":1381.458942918518}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:06+00","p50":1531.2774975,"p95":1727.0988918096905,"p99":1740.6296473294506}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:07+00","p50":1721.26488675,"p95":1922.524600609455,"p99":1943.9292204269705}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:08+00","p50":1802.5658937500002,"p95":2024.5473021773128,"p99":2054.887496144639}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:09+00","p50":1978.882456,"p95":2191.902895516285,"p99":2205.1589590132908}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:10+00","p50":1990.6579725,"p95":2209.3159734986725,"p99":2238.0826819135714}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:11+00","p50":2176.4690714999997,"p95":2292.1497635079963,"p99":2303.916270833235}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:12+00","p50":2324.7444459999997,"p95":2544.367692183368,"p99":2549.848348520047}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:13+00","p50":2446.0844977499996,"p95":2589.530504181544,"p99":2609.0245395498077}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:14+00","p50":2504.683462125,"p95":2651.2530719654264,"p99":2680.9332736875335}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:15+00","p50":2680.0950148750003,"p95":2741.7838358323156,"p99":2768.2416541217744}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:16+00","p50":2810.4141692499998,"p95":3040.027586211146,"p99":3067.2793681274493}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:17+00","p50":2906.57442975,"p95":3041.724965315609,"p99":3075.419855225094}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:18+00","p50":3120.73932075,"p95":3199.255901603081,"p99":3220.4518787051666}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:19+00","p50":3233.4853369999996,"p95":3438.1120182096033,"p99":3465.277620075524}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:20+00","p50":3446.04431375,"p95":3601.861288024976,"p99":3615.8428149645874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:21+00","p50":3595.5924704999998,"p95":3900.2569235455717,"p99":3975.1193913844636}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:22+00","p50":3531.61714075,"p95":4071.1654271523316,"p99":4486.923778539505}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:23+00","p50":3687.4838600000003,"p95":4086.7900667323283,"p99":4100.71143191364}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:24+00","p50":3611.33292075,"p95":4074.1877501210597,"p99":4509.930734696066}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:25+00","p50":3638.103952125,"p95":4072.8700214423247,"p99":4573.41064654972}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:26+00","p50":3510.234285125,"p95":4061.9114029906805,"p99":4437.98522896557}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:27+00","p50":3601.5576490000003,"p95":3927.2926392530594,"p99":4387.442760254505}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:28+00","p50":3511.834971,"p95":3957.426548973483,"p99":3963.6552373156173}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:29+00","p50":3867.2986204999997,"p95":4063.7594057928427,"p99":4476.03004764843}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:30+00","p50":3556.5346731249997,"p95":4040.157939341941,"p99":4439.21579197665}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:31+00","p50":3547.3357925,"p95":3922.01176201694,"p99":3942.629515900821}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:32+00","p50":3565.428602,"p95":3674.8580207980353,"p99":3720.5394602779356}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:33+00","p50":3404.256337625,"p95":3729.3204558256125,"p99":4119.504816867131}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:34+00","p50":3396.5651429999994,"p95":3850.461258424381,"p99":4299.858963738588}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:35+00","p50":3411.676362,"p95":3843.320541621439,"p99":4326.075394919133}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:36+00","p50":3212.18869575,"p95":3631.597678906604,"p99":4052.2754372288546}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:37+00","p50":3543.3518205,"p95":3802.4114436087457,"p99":4144.388407717189}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:38+00","p50":3277.927658125,"p95":3702.199795814139,"p99":3717.452258678274}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:39+00","p50":3391.0351877499997,"p95":3755.0789200971144,"p99":4166.348248277748}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:40+00","p50":3334.3780690000003,"p95":3615.911845278965,"p99":4036.046196287218}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:41+00","p50":3235.634038,"p95":3511.7357670321,"p99":3533.479011059226}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:42+00","p50":3264.74354625,"p95":3559.9847295614622,"p99":3605.2525032476674}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:43+00","p50":3525.801193625,"p95":3661.151233722853,"p99":3718.1446374812895}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:44+00","p50":3527.277897,"p95":3610.947534595779,"p99":3784.7907925760046}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:45+00","p50":3577.828263125,"p95":3633.0049828603137,"p99":3751.6392640339077}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:46+00","p50":3463.34763775,"p95":3585.4146807282923,"p99":3611.8553884622656}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:47+00","p50":3366.5365125,"p95":3520.566598874537,"p99":3765.04485589119}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:48+00","p50":3463.1514859999997,"p95":3559.112490061339,"p99":3696.2986221402493}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:49+00","p50":3500.744176875,"p95":3732.8167910003176,"p99":3767.479247207812}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:50+00","p50":3585.1259915,"p95":3736.1974891706495,"p99":3774.462153419137}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:51+00","p50":3747.2525849999997,"p95":3881.1657774875243,"p99":4137.93943490527}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:52+00","p50":3649.71790125,"p95":3823.381431391973,"p99":3842.540053848862}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:53+00","p50":3690.2178052500003,"p95":3798.6987488999443,"p99":3897.5215393892554}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:54+00","p50":3735.656183875,"p95":3830.9000724477905,"p99":4059.697179538623}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:55+00","p50":3600.54081975,"p95":3805.822567617779,"p99":3889.993837703535}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:56+00","p50":3801.22329,"p95":4029.104522612628,"p99":4061.611025709741}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:57+00","p50":3785.339486375,"p95":3967.7904143999876,"p99":4168.890813885157}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:58+00","p50":3694.8809123749998,"p95":3857.7252110720683,"p99":4158.216895944797}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:24:59+00","p50":3656.3428513749996,"p95":3763.213065953433,"p99":3880.4236329015034}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:00+00","p50":3593.82109925,"p95":3672.6909742913763,"p99":3852.0690457144487}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:01+00","p50":3478.9615205,"p95":3557.932504728273,"p99":3709.1024702415716}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:02+00","p50":3521.4678653749997,"p95":3603.2595871204285,"p99":3740.4721125684705}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:03+00","p50":3581.76149075,"p95":3697.0128710797,"p99":3922.682242458376}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:04+00","p50":3516.225599375,"p95":3588.9021518697486,"p99":3810.2248513898194}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:05+00","p50":3427.34066,"p95":3582.2956253082693,"p99":3707.973709135007}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:06+00","p50":3420.0794214999996,"p95":3569.172599891657,"p99":3609.4501495733407}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:07+00","p50":3462.9104095000002,"p95":3565.727492054663,"p99":3835.851189270111}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:08+00","p50":3622.8823859999998,"p95":3772.9689387840695,"p99":3885.3832955724215}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:09+00","p50":3730.57883875,"p95":3843.1932471034984,"p99":3877.244675922702}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:10+00","p50":3679.8032942500004,"p95":3842.776543901489,"p99":4150.106671810617}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:11+00","p50":3567.590359625,"p95":3703.505146494678,"p99":3758.7888960016003}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:12+00","p50":3477.197212375,"p95":3551.751535620048,"p99":3884.9633133121397}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:13+00","p50":3539.3050026250003,"p95":3669.6216680028297,"p99":3699.549754191283}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:14+00","p50":3495.195182375,"p95":3695.808355233249,"p99":3731.295772108157}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:15+00","p50":3566.30512425,"p95":3725.491952808226,"p99":3902.9979797030924}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:16+00","p50":3488.08302,"p95":3689.4996533210146,"p99":3702.294636893147}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:17+00","p50":3415.85412875,"p95":3659.5651509388804,"p99":3709.086615620174}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:18+00","p50":3529.311957,"p95":3857.737303678934,"p99":4020.9337523655904}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:19+00","p50":3730.693097,"p95":4003.8014889041874,"p99":4266.601698436933}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:20+00","p50":3895.867279125,"p95":4148.039803250536,"p99":4348.727380626039}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:21+00","p50":3906.791890875,"p95":4168.79965361671,"p99":4177.2039238115285}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:22+00","p50":3593.226044,"p95":4340.426528809637,"p99":4911.210693036717}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:23+00","p50":3833.82376425,"p95":4288.753260677971,"p99":4300.924484395844}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:24+00","p50":3287.726313,"p95":4046.1809068847474,"p99":4617.68459322019}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:25+00","p50":3331.8465665,"p95":3819.415670170718,"p99":3889.5554152173236}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:26+00","p50":3481.90592625,"p95":3880.708875222845,"p99":3899.90477281493}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:27+00","p50":3270.7418795,"p95":3887.0462382685455,"p99":3929.7854237787255}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:28+00","p50":3601.68637725,"p95":3909.258552455942,"p99":4426.952432421562}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:29+00","p50":3201.3998465,"p95":3935.7045556175494,"p99":3963.3256527659514}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:30+00","p50":3126.6861416249994,"p95":4147.59832476965,"p99":4215.301285406424}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:31+00","p50":3225.591723625,"p95":4180.825460586614,"p99":4920.273588741885}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:32+00","p50":3695.8252138750004,"p95":4215.082598274665,"p99":4844.977716592199}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:33+00","p50":3117.2507425000003,"p95":4197.1613349728805,"p99":4233.814391560235}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:34+00","p50":3204.7169485000004,"p95":4085.515345695571,"p99":4788.421946637182}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:35+00","p50":3503.725206,"p95":4096.870886357378,"p99":4610.343151941729}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:36+00","p50":3920.87825,"p95":4041.912433317259,"p99":4680.303925122076}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:37+00","p50":3730.357664,"p95":4332.999375126092,"p99":4340.197223292382}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:38+00","p50":4191.6121825,"p95":4289.618204621079,"p99":4350.830015056537}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:39+00","p50":3382.6004305,"p95":4208.675556578914,"p99":4218.971652223488}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:40+00","p50":3938.884211875,"p95":4222.380188991984,"p99":4260.318794568973}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:41+00","p50":3333.0664575,"p95":3899.4723639318745,"p99":4748.727669736752}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:42+00","p50":3145.8303967499996,"p95":3799.1003704271034,"p99":3819.6899984406637}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:43+00","p50":2965.4569058750003,"p95":3848.2365355501142,"p99":4498.7474963259665}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:44+00","p50":3154.624902875,"p95":3967.2420468831956,"p99":4115.922302840257}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:45+00","p50":3509.041949,"p95":3965.412179053796,"p99":3997.0430946229676}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:46+00","p50":3157.6377679999996,"p95":3914.156320334858,"p99":4019.738916727091}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:47+00","p50":3318.22231075,"p95":3865.841038522584,"p99":4337.183490509892}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:48+00","p50":3216.854437,"p95":3869.8091404796464,"p99":4496.053587076728}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:49+00","p50":3260.02860975,"p95":3960.5863986227096,"p99":4573.78473805164}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:50+00","p50":3336.9933735,"p95":3879.95285561012,"p99":4379.707942109437}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:51+00","p50":3450.65681,"p95":4056.667577822483,"p99":4555.344308004432}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:52+00","p50":4092.255165,"p95":4311.429199183736,"p99":4443.26033698884}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:53+00","p50":3847.6867735,"p95":4257.114009501214,"p99":4888.372164108224}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:54+00","p50":3610.720882,"p95":4247.279180728638,"p99":4264.352095463552}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:55+00","p50":3552.026827,"p95":4015.6639999559106,"p99":4099.211796838768}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:56+00","p50":3316.125102,"p95":3983.042612748443,"p99":4432.465908144368}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:57+00","p50":3541.59546025,"p95":3984.1534312749213,"p99":3995.2669046785127}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:58+00","p50":3546.9275475,"p95":4146.647575301836,"p99":4177.285343882656}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:25:59+00","p50":3616.22071825,"p95":4226.460321547764,"p99":4696.934307374805}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:00+00","p50":3662.15952475,"p95":4098.797559146975,"p99":4119.477353739493}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:01+00","p50":3526.8627145,"p95":3785.517651456736,"p99":3840.3341122087604}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:02+00","p50":3474.251604,"p95":3562.105973708498,"p99":3739.36088886145}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:03+00","p50":3451.98868225,"p95":3509.13816836713,"p99":3619.4416864623786}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:04+00","p50":3508.92612375,"p95":3611.301713968859,"p99":3797.4619522869166}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:05+00","p50":3457.1430178749997,"p95":3561.6296253475484,"p99":3678.035409742035}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:06+00","p50":3495.966733875,"p95":3681.0627334183505,"p99":3744.1594845905984}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:07+00","p50":3452.77780325,"p95":3681.98401121583,"p99":3703.9825268366308}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:08+00","p50":3423.087943,"p95":3499.219289493744,"p99":3582.903040981125}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:09+00","p50":3462.26961725,"p95":3589.4144397112013,"p99":3613.9672255248156}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:10+00","p50":3468.10017625,"p95":3584.7065455348343,"p99":3739.062943692101}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:11+00","p50":3466.823829,"p95":3635.429398250429,"p99":3658.564746296943}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:12+00","p50":3405.45864825,"p95":3634.26954971359,"p99":3948.6261402218893}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:13+00","p50":3216.76469725,"p95":3666.7651342110075,"p99":4040.860831256069}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:14+00","p50":2978.462568875,"p95":3527.841870642037,"p99":3543.545944516956}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:15+00","p50":3451.837638625,"p95":3572.9070627645046,"p99":4005.784093757491}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:16+00","p50":3270.5569258749997,"p95":3763.790043609476,"p99":4009.8750724680185}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:17+00","p50":3423.68553575,"p95":3796.266196514504,"p99":4270.476355905004}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:18+00","p50":3538.264716375,"p95":3975.735486033632,"p99":4641.373530554107}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:19+00","p50":3783.0034138750007,"p95":3991.9284348751826,"p99":4652.030303113633}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:20+00","p50":3428.6354485,"p95":4080.7964455825218,"p99":4119.456860625247}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:21+00","p50":3466.271803,"p95":4254.376775088003,"p99":4392.439406620374}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:22+00","p50":4145.648621,"p95":4297.7983504262065,"p99":4881.6221973181355}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:23+00","p50":3453.809624375,"p95":4235.284208436033,"p99":4256.587256144354}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:24+00","p50":3544.632219375,"p95":3889.0094918878794,"p99":4504.422645498615}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:25+00","p50":3133.4848185,"p95":3831.3655840961887,"p99":3882.523913650601}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:26+00","p50":3535.5932433750004,"p95":3882.268604893289,"p99":4283.098898556368}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:27+00","p50":3291.7177245,"p95":3930.163065630021,"p99":4510.698082904734}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:28+00","p50":3374.5027785,"p95":4028.3962788353683,"p99":4510.735762242086}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:29+00","p50":3892.1633745,"p95":4054.1424457132707,"p99":4532.134821206853}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:30+00","p50":3324.0909887499997,"p95":3910.8101418382203,"p99":4375.267382739267}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:31+00","p50":3217.6968975,"p95":3813.7957852076265,"p99":4329.917330557441}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:32+00","p50":3476.533146875,"p95":3693.052141918579,"p99":3771.4608053531256}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:33+00","p50":3204.668976125,"p95":3889.567209955813,"p99":3913.6848181076816}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:34+00","p50":3380.404363,"p95":3849.7269721928114,"p99":3923.2834467936123}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:35+00","p50":2983.3259829999997,"p95":3750.6758693588727,"p99":4198.739128717658}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:36+00","p50":3045.4717252500004,"p95":3789.0832692432346,"p99":3823.5446670458327}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:37+00","p50":3559.7000635000004,"p95":3796.933455706567,"p99":4379.93753667425}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:38+00","p50":3301.911508,"p95":3922.615234142419,"p99":3943.220385344261}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:39+00","p50":3569.066047375,"p95":3980.649639555351,"p99":4010.4678052974264}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:40+00","p50":2936.1808815,"p95":3742.3617341416493,"p99":4217.976285432411}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:41+00","p50":3486.04881625,"p95":3755.914072270731,"p99":4357.341197964417}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:42+00","p50":3412.09496925,"p95":3769.1358189200187,"p99":4438.516250391419}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:43+00","p50":3262.5935755,"p95":3812.5355542915327,"p99":3839.668564269691}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:44+00","p50":3465.8458742499997,"p95":3875.9388805476074,"p99":4214.301094649151}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:45+00","p50":3207.36485175,"p95":3695.340292925361,"p99":3716.802749822253}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:46+00","p50":3384.9756196250005,"p95":3706.876177058282,"p99":3727.856590710151}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:47+00","p50":3568.251657,"p95":3688.181905724153,"p99":4171.133369482545}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:48+00","p50":2978.566192625,"p95":3931.2455632957835,"p99":3983.6640576241}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:49+00","p50":3670.2205672500004,"p95":3992.01932885076,"p99":4009.935001411243}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:50+00","p50":3673.9656728749997,"p95":3922.2200602978937,"p99":4491.003575866322}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:51+00","p50":3315.68537575,"p95":3867.2627854427974,"p99":4360.050515721922}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:52+00","p50":3449.0460439999997,"p95":3736.468532382954,"p99":3749.0563372721285}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:53+00","p50":3349.462993,"p95":3764.3428174321366,"p99":4263.014077979499}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:54+00","p50":3744.2229975,"p95":3927.321984617036,"p99":4426.456299050883}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:55+00","p50":3400.7016447500005,"p95":3848.735568202755,"p99":3912.464613249975}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:56+00","p50":3484.68829975,"p95":3737.3927293787283,"p99":3753.6285136706383}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:57+00","p50":3731.284213,"p95":3920.3408071203608,"p99":4385.905862223789}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:58+00","p50":3781.2445290000005,"p95":4090.459814546657,"p99":4570.615689405758}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:26:59+00","p50":3525.08254675,"p95":4224.417241069516,"p99":4255.712336167622}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:00+00","p50":3623.1936033750003,"p95":4133.104747764275,"p99":4164.522474699911}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:01+00","p50":3520.877783625,"p95":3986.294386601893,"p99":4002.8895375813404}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:02+00","p50":3082.652722,"p95":3719.2984090220275,"p99":4406.963595812104}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:03+00","p50":3274.186785,"p95":3517.637881479286,"p99":3900.756921351408}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:04+00","p50":3287.79080875,"p95":3583.340648100553,"p99":3838.092452572751}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:05+00","p50":3620.855594875,"p95":3951.1854957047144,"p99":4009.3561775390626}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:06+00","p50":3680.4248975,"p95":4059.783124925266,"p99":4614.452949989911}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:07+00","p50":3115.829333,"p95":3967.891059217614,"p99":3987.7577547969686}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:08+00","p50":3193.2945609999997,"p95":4187.21128918656,"p99":4510.32733838227}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:09+00","p50":3414.02003075,"p95":4133.778974696107,"p99":4148.451435665658}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:10+00","p50":3011.9273169999997,"p95":4011.812951370468,"p99":4458.402436701028}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:11+00","p50":2931.8864653749997,"p95":4150.360537729698,"p99":4723.901797395476}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:12+00","p50":3123.8275803750003,"p95":4102.141109958721,"p99":4118.3587737129865}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:13+00","p50":3179.8711137500004,"p95":3981.1310814125914,"p99":4693.530909740361}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:14+00","p50":3697.9281685,"p95":3903.1736238939584,"p99":4355.042440476161}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:15+00","p50":3451.07247725,"p95":3894.1496653366667,"p99":3906.1837873056675}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:16+00","p50":2693.4826555,"p95":3940.4119734507503,"p99":3949.831742593146}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:17+00","p50":3280.062521,"p95":3848.124274738824,"p99":4463.531678095536}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:18+00","p50":3828.022309125,"p95":3942.0613821552683,"p99":4065.2977380436764}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:19+00","p50":3271.6145435,"p95":4099.9645441288,"p99":4702.907016647326}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:20+00","p50":3716.277994,"p95":4042.054178503022,"p99":4056.544916928241}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:21+00","p50":3447.0748445000004,"p95":4022.843055763256,"p99":4604.569823573927}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:22+00","p50":3449.2186565,"p95":4065.690511914529,"p99":4124.39585462843}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:23+00","p50":2848.2566435,"p95":4234.060869710547,"p99":4336.816513083303}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:24+00","p50":3565.1620625,"p95":4501.646320939316,"p99":5230.272315735413}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:25+00","p50":3662.6661225,"p95":4507.397769166497,"p99":5284.740834513771}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:26+00","p50":3336.98678975,"p95":4535.0777414950035,"p99":5426.519119411238}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:27+00","p50":3202.079413,"p95":4592.179705147665,"p99":4641.550942663603}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:28+00","p50":2807.361602,"p95":4703.6515559974405,"p99":5013.236447927157}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:29+00","p50":4141.84793475,"p95":4728.204158097778,"p99":4779.147222228986}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:30+00","p50":3188.2410207499997,"p95":4615.120880382133,"p99":5552.412984206742}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:31+00","p50":3149.5639755,"p95":4591.127935578073,"p99":4614.639645369157}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:32+00","p50":3895.2964655,"p95":4567.837058060886,"p99":4576.7093396090395}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:33+00","p50":3175.7740986249996,"p95":4322.273504506343,"p99":5362.988139275996}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:34+00","p50":3765.9603016250003,"p95":4275.25906082934,"p99":4292.744070737092}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:35+00","p50":3279.223076,"p95":4138.651095101575,"p99":4148.426918472281}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:36+00","p50":2434.145629,"p95":4029.8645447361755,"p99":4721.7604647876515}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:37+00","p50":2463.541052,"p95":4086.800274504963,"p99":4769.701341475042}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:38+00","p50":2524.211697375,"p95":4055.88536989256,"p99":4841.744313016671}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:39+00","p50":3067.7395397500004,"p95":3972.969639681564,"p99":3993.738852459028}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:40+00","p50":2766.8504285,"p95":3982.7972042919355,"p99":4004.6629798846748}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:41+00","p50":3636.1022924999997,"p95":4162.285063294626,"p99":4845.708463817312}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:42+00","p50":4055.212262,"p95":4268.761679237702,"p99":4502.613898700589}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:43+00","p50":3607.4822945,"p95":4409.150439476935,"p99":4540.172153440326}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:44+00","p50":3044.447666,"p95":4468.971432375423,"p99":5161.023594378186}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:45+00","p50":3118.287975,"p95":4497.718578279316,"p99":5293.9103669841425}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:46+00","p50":3094.35594175,"p95":4463.320350489905,"p99":4488.201086385752}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:47+00","p50":2641.914557375,"p95":4380.522250353071,"p99":5229.317148735334}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:48+00","p50":3683.290513125,"p95":4306.729330501255,"p99":4420.724673599374}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:49+00","p50":3229.840401,"p95":4355.075958627235,"p99":4371.235201101437}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:50+00","p50":3610.3604232499997,"p95":4113.112089361059,"p99":4144.417610289678}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:51+00","p50":4066.0047405,"p95":4152.605584976633,"p99":4166.360746039604}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:52+00","p50":3085.9163945,"p95":4155.236073568628,"p99":5005.853848865874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:53+00","p50":3375.4413045,"p95":4075.1839548203034,"p99":4088.1898143933627}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:54+00","p50":3458.58467775,"p95":4102.9782237028,"p99":4859.939918991129}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:55+00","p50":3680.838240125,"p95":4074.8185499090264,"p99":4107.601261573322}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:56+00","p50":2964.1684164999997,"p95":4366.261094754104,"p99":5003.9684712654725}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:57+00","p50":2865.889019,"p95":4395.597074494181,"p99":4553.004994506878}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:58+00","p50":3932.65645275,"p95":4442.276454465319,"p99":5190.443685607958}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:27:59+00","p50":4410.925996624999,"p95":4586.220768876543,"p99":5278.955793505864}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:00+00","p50":4034.1972052499996,"p95":4559.130298201867,"p99":5185.447504619028}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:01+00","p50":3011.5971793749995,"p95":4228.725838953624,"p99":4241.727052040938}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:02+00","p50":4103.134358125,"p95":4230.320376282643,"p99":4289.097515150797}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:03+00","p50":3657.1021259999998,"p95":4116.425072436185,"p99":4151.895207115409}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:04+00","p50":2763.4415573750002,"p95":4148.461006578822,"p99":4977.89127049169}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:05+00","p50":2803.0761302500005,"p95":4150.987627834745,"p99":4168.726618227346}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:06+00","p50":3292.065742,"p95":4123.164492521364,"p99":4955.7790042752995}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:07+00","p50":2580.638828,"p95":4048.9433103377046,"p99":4891.489063014715}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:08+00","p50":3339.9213505000002,"p95":3925.5697933987967,"p99":3990.0480407188943}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:09+00","p50":3839.2678484999997,"p95":4006.4820711769908,"p99":4626.656186085876}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:10+00","p50":3679.886141125,"p95":4039.8164735142022,"p99":4630.071189136292}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:11+00","p50":3035.3440645,"p95":4066.975300062276,"p99":4083.543306899182}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:12+00","p50":3026.4333752499997,"p95":3868.8842505985494,"p99":3883.736851088003}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:13+00","p50":3027.374650625,"p95":3904.142149395573,"p99":4623.29563806761}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:14+00","p50":3385.7517473749995,"p95":3922.026432154607,"p99":4486.640380948272}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:15+00","p50":3288.0399959999995,"p95":3914.434493488434,"p99":4408.0731495436075}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:16+00","p50":3433.456347125,"p95":3693.8189546066706,"p99":3707.019133543986}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:17+00","p50":3318.8951985,"p95":3512.2413586116086,"p99":3778.96088502609}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:18+00","p50":3463.0845868750002,"p95":3548.0562155984767,"p99":3892.7022433865027}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:19+00","p50":3593.128539,"p95":3681.4846649603064,"p99":3693.504979166574}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:20+00","p50":3666.02516925,"p95":3808.9027289216974,"p99":3995.606134938272}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:21+00","p50":3803.53743975,"p95":3934.2575236848793,"p99":3955.063909196102}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:22+00","p50":3636.8879850000003,"p95":3685.6786864425917,"p99":3827.5629082065}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:23+00","p50":3556.572298375,"p95":3673.446168850147,"p99":3720.690510570016}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:24+00","p50":3547.5891488750003,"p95":3741.1515211827727,"p99":3938.645933900948}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:25+00","p50":3611.36273475,"p95":3743.5796807435263,"p99":3757.9878752235695}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:26+00","p50":3547.4878625,"p95":3777.185178089711,"p99":3796.7612253286948}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:27+00","p50":3819.4086505,"p95":3900.0197039014943,"p99":3917.339710490801}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:28+00","p50":3653.93620575,"p95":3851.0810812886075,"p99":3909.8180805517936}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:29+00","p50":3547.474312,"p95":3853.888951934024,"p99":4228.825918707601}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:30+00","p50":3673.090513375,"p95":3933.772025138768,"p99":3961.383521813943}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:31+00","p50":3646.32842125,"p95":4037.3945035014376,"p99":4433.832465810697}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:32+00","p50":3646.63651325,"p95":3878.2468072237016,"p99":3957.6228089642477}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:33+00","p50":3482.256512875,"p95":3898.592873227888,"p99":4332.38287754631}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:34+00","p50":3715.1027705,"p95":3883.163828435888,"p99":3903.334915458724}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:35+00","p50":3626.09017,"p95":3747.28729952866,"p99":4072.739555775986}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:36+00","p50":3504.78784125,"p95":3724.4332945109763,"p99":3748.906499040899}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:37+00","p50":3364.3109560000003,"p95":3602.926172271296,"p99":3903.332412706989}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:38+00","p50":3328.188500125,"p95":3625.342094282779,"p99":3659.797753185689}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:39+00","p50":3526.28146525,"p95":3625.416153748704,"p99":3669.521553838462}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:40+00","p50":3539.6544965000003,"p95":3669.7782621074916,"p99":3686.2912794919625}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:41+00","p50":3433.658317,"p95":3672.1281702503597,"p99":3846.549322790142}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:42+00","p50":3424.02914325,"p95":3540.322985194542,"p99":3580.5066539711293}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:43+00","p50":3473.5075490000004,"p95":3594.012852838033,"p99":3866.5131640161185}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:44+00","p50":3577.94262825,"p95":3713.405431260119,"p99":3967.5253950677784}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:45+00","p50":3623.019509625,"p95":3747.3386125325833,"p99":3762.7447929644522}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:46+00","p50":3472.405076125,"p95":3661.063269547953,"p99":3679.4082031409625}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:47+00","p50":3531.922920125,"p95":3636.8129183614037,"p99":3943.111597543816}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:48+00","p50":3567.61525225,"p95":3600.604094396395,"p99":3711.5057146080585}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:49+00","p50":3595.696381,"p95":3678.4680473908447,"p99":3844.1880108563837}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:50+00","p50":3659.194686,"p95":3943.8354405010928,"p99":3961.032038300077}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:51+00","p50":3816.9452677500003,"p95":3987.910024962801,"p99":4457.230932217657}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:52+00","p50":3524.6376565,"p95":3878.8138257862784,"p99":4010.0858986879625}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:53+00","p50":3495.5561037499997,"p95":3698.6958088375964,"p99":3827.8161783620913}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:54+00","p50":3568.069455,"p95":3635.563799979515,"p99":3854.977818305993}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:55+00","p50":3516.280917875,"p95":3648.5535024793758,"p99":3858.115864831183}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:56+00","p50":3584.802883625,"p95":3752.711252032388,"p99":3977.0467856968676}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:57+00","p50":3791.3392423749997,"p95":3899.741819957215,"p99":3921.8197951966936}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:58+00","p50":3804.2268405,"p95":3892.8741881986025,"p99":4193.522712203408}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:28:59+00","p50":3772.779025875,"p95":3848.634807779939,"p99":4034.3663330440595}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:00+00","p50":3688.761032,"p95":3797.8016701499237,"p99":3820.707296793787}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:01+00","p50":3566.7728357499996,"p95":3700.582713124283,"p99":3861.647357473218}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:02+00","p50":3431.6885855,"p95":3559.8934418719214,"p99":3586.437213299349}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:03+00","p50":3444.4297642499996,"p95":3548.104882742185,"p99":3558.3078711155576}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:04+00","p50":3449.5454847499996,"p95":3509.2078925037586,"p99":3677.442849478374}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:05+00","p50":3432.969422,"p95":3585.9773962116087,"p99":3812.7791886988666}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:06+00","p50":3385.6768041249998,"p95":3437.62593300624,"p99":3452.923355927612}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:07+00","p50":3365.353478,"p95":3449.027836245853,"p99":3470.7601149166258}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:08+00","p50":3384.06995875,"p95":3599.238188169994,"p99":3920.171499552074}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:09+00","p50":3466.751639,"p95":3550.3298107587566,"p99":3564.5230005394337}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:10+00","p50":3435.7687457499997,"p95":3629.3807054376593,"p99":3856.972695671851}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:11+00","p50":3427.8446672500004,"p95":3617.171250982942,"p99":3905.0450997520643}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:12+00","p50":3319.7092247500004,"p95":3449.7410277844237,"p99":3782.1997797476124}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:13+00","p50":3296.379978,"p95":3421.3316906169525,"p99":3438.8387074681164}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:14+00","p50":3340.078062875,"p95":3547.491117987939,"p99":3919.5914898818464}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:15+00","p50":3396.84253425,"p95":3644.361844272291,"p99":3965.997655246718}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:16+00","p50":3314.7772426250003,"p95":3793.8323918732494,"p99":3816.720455724613}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:17+00","p50":3664.6801175,"p95":3771.2400644662885,"p99":3814.2431064847733}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:18+00","p50":2970.05509525,"p95":3756.9493727034037,"p99":3770.7239510243335}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:19+00","p50":3406.6905168750004,"p95":3623.3918284571923,"p99":3630.857669171109}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:20+00","p50":3462.02766675,"p95":3562.1929407189596,"p99":3576.0775128654645}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:21+00","p50":3448.8291664999997,"p95":3625.014267541107,"p99":3670.3987218334873}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:22+00","p50":3093.67880375,"p95":3683.8364556616307,"p99":3691.188668141439}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:23+00","p50":3551.86223375,"p95":3937.9084114066845,"p99":3963.0343584300426}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:24+00","p50":3615.01487825,"p95":3886.5086144468537,"p99":3907.8740527920427}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:25+00","p50":3558.26084825,"p95":3767.3591130049076,"p99":4196.512270505951}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:26+00","p50":3603.69071175,"p95":3725.7669706474976,"p99":3738.5588317765846}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:27+00","p50":3638.0263365,"p95":3681.4790785114083,"p99":3711.592017467467}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:28+00","p50":3703.4514023750003,"p95":3750.9500408612716,"p99":3776.180268810701}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:29+00","p50":3700.21863475,"p95":3784.494804608685,"p99":4029.5784945081837}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:30+00","p50":3538.573892,"p95":3613.3840873774284,"p99":3732.3988825778265}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:31+00","p50":3538.86084625,"p95":3635.4988957976734,"p99":3900.496244690122}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:32+00","p50":3516.382668,"p95":3579.7279365663326,"p99":3778.3491986757767}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:33+00","p50":3579.2845185,"p95":3704.6653848092205,"p99":3797.7316673765886}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:34+00","p50":3653.35149675,"p95":3764.380155541877,"p99":4057.936174761084}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:35+00","p50":3613.9411,"p95":3737.765302559593,"p99":3778.458492479302}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:36+00","p50":3672.89734775,"p95":3877.3260690620564,"p99":4101.670996034614}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:37+00","p50":3520.273679,"p95":3750.0616688646996,"p99":3818.397921777375}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:38+00","p50":3645.006872,"p95":3709.2413380146377,"p99":3737.698825677605}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:39+00","p50":3758.9418692500003,"p95":3879.096143108637,"p99":3931.138178513101}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:40+00","p50":3753.9209282499996,"p95":3874.3002795345355,"p99":3894.149142010481}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:41+00","p50":3588.69363925,"p95":3856.1687408475464,"p99":3878.578244820264}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:42+00","p50":3174.0661005,"p95":3854.4274433046926,"p99":3868.1208499058257}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:43+00","p50":3769.1534225,"p95":3864.1340222131034,"p99":3943.599155576057}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:44+00","p50":3489.8001145,"p95":3913.8541582272437,"p99":4303.158807945632}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:45+00","p50":3607.1359162500003,"p95":3952.8888411546695,"p99":4344.660952660999}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:46+00","p50":2900.04061525,"p95":3873.3870833779793,"p99":3928.5451709075164}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:47+00","p50":3390.410042,"p95":3937.2915578474294,"p99":3955.2598776538775}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:48+00","p50":3600.0552074999996,"p95":3894.681874970238,"p99":4438.77431078708}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:49+00","p50":3636.1740422499997,"p95":3956.042540986017,"p99":3981.485268019741}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:50+00","p50":3170.713195,"p95":4026.438612038984,"p99":4039.9455398144573}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:51+00","p50":3223.26103825,"p95":3987.565636051262,"p99":4525.035055974745}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:52+00","p50":3015.157203,"p95":3902.057738334162,"p99":3920.569782444831}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:53+00","p50":3257.9592945,"p95":3932.0984674200536,"p99":3969.934136344074}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:54+00","p50":3598.7055375,"p95":3959.1146370446177,"p99":4478.114884146608}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:55+00","p50":3784.833992875,"p95":3883.8546643058858,"p99":4349.283182893325}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:56+00","p50":3283.831603,"p95":3955.525661386857,"p99":4023.1440445961553}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:57+00","p50":3932.5875617499996,"p95":4052.9663898138974,"p99":4635.352224858722}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:58+00","p50":3678.409009625,"p95":4020.52824899742,"p99":4156.540454227063}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:29:59+00","p50":3585.063102375,"p95":4148.10856011942,"p99":4628.281332502198}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:00+00","p50":3571.11671625,"p95":3968.712178497662,"p99":4313.3894986451605}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:01+00","p50":3721.8158115,"p95":3963.1705937586444,"p99":3977.9668797044715}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:02+00","p50":3593.197560875,"p95":3959.8680593809245,"p99":3966.2824208634474}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:03+00","p50":3394.98701175,"p95":3923.6983288553574,"p99":4427.229032228105}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:04+00","p50":3330.43031075,"p95":3892.5929057568487,"p99":4304.607278944129}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:05+00","p50":3404.7007197499997,"p95":3813.421364891845,"p99":4323.831563085202}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:06+00","p50":3393.01707025,"p95":3765.6991832799695,"p99":3789.252629159957}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:07+00","p50":3263.6520606249996,"p95":3791.0766566813572,"p99":3820.3819668661995}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:08+00","p50":3292.2722135,"p95":3873.5916309620766,"p99":3883.405000262578}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:09+00","p50":3711.708292875,"p95":3836.4561227864015,"p99":3856.2581809447756}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:10+00","p50":3428.56717475,"p95":3747.68158276652,"p99":3792.2104701007815}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:11+00","p50":3552.143066,"p95":3710.8208404156526,"p99":3722.649712579158}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:12+00","p50":3504.49375975,"p95":3719.6849350633784,"p99":3852.84108190447}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:13+00","p50":3385.0746907499997,"p95":3556.165147499126,"p99":3565.9427482578812}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:14+00","p50":3449.8145427500003,"p95":3646.3161686658946,"p99":3911.5978840696575}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:15+00","p50":3512.60210375,"p95":3661.8836973741686,"p99":3677.3268786784415}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:16+00","p50":3520.382777875,"p95":3589.799536888603,"p99":3762.612097913052}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:17+00","p50":3528.1434364999996,"p95":3649.718664055612,"p99":3823.6693134825505}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:18+00","p50":3540.69936275,"p95":3607.489572856269,"p99":3799.291476688115}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:19+00","p50":3603.4981071249995,"p95":3691.1301691139934,"p99":3716.370938600763}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:20+00","p50":3623.38861025,"p95":3725.4417121826236,"p99":3938.8931251192685}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:21+00","p50":3693.1377672500003,"p95":3986.6886197758126,"p99":4005.434536730856}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:22+00","p50":3755.02770675,"p95":3943.6678130994123,"p99":4011.592966525638}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:23+00","p50":3731.230289,"p95":3919.584015188537,"p99":3961.160745364471}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:24+00","p50":3746.813976,"p95":3886.171622555736,"p99":4159.9598887954135}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:25+00","p50":3686.87130125,"p95":3754.4529349503073,"p99":4013.639100732556}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:26+00","p50":3596.79155825,"p95":3746.8232662865166,"p99":3755.185932770833}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:27+00","p50":3594.3918316249997,"p95":3754.9875756181355,"p99":3765.7770275954567}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:28+00","p50":3644.53205975,"p95":3697.27017009282,"p99":3932.7093459127545}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:29+00","p50":3601.9482262499996,"p95":3677.954062759721,"p99":3840.8272072784457}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:30+00","p50":3493.7734805,"p95":3584.06795952398,"p99":3699.851552494332}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:31+00","p50":3578.970503,"p95":3724.65634976704,"p99":3876.9230666744975}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:32+00","p50":3609.7236205,"p95":3718.4860037934527,"p99":3986.273353610964}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:33+00","p50":3469.225235,"p95":3749.0336871638906,"p99":3987.550665525765}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:34+00","p50":3456.398018,"p95":3706.6487629282115,"p99":3724.9603008537874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:35+00","p50":3382.4069842500003,"p95":3510.9850437722594,"p99":3517.3821440669094}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:36+00","p50":3490.159540375,"p95":3572.977426724732,"p99":3727.2271411921065}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:37+00","p50":3497.1205794999996,"p95":3548.9000149122617,"p99":3650.661530585488}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:38+00","p50":3545.2308651249996,"p95":3602.494096926326,"p99":3901.426857825637}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:39+00","p50":3555.4742845,"p95":3657.4074090950853,"p99":3678.0038774550953}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:40+00","p50":3429.34002525,"p95":3673.221635769465,"p99":3987.066192597272}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:41+00","p50":3409.4688465,"p95":3668.9180727852236,"p99":3679.1062104932976}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:42+00","p50":3403.2093671250004,"p95":3623.9178847046005,"p99":3977.3641117117463}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:43+00","p50":3418.3141130000004,"p95":3632.970243394095,"p99":3715.2834646770484}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:44+00","p50":3517.75212475,"p95":3678.9224342147713,"p99":4070.0769598861366}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:45+00","p50":3449.31020025,"p95":3557.047608245414,"p99":3632.1175847927184}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:46+00","p50":3325.9133035000004,"p95":3448.5619214787844,"p99":3600.360503442284}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:47+00","p50":3287.6495315,"p95":3341.765712088595,"p99":3482.5604690657315}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:48+00","p50":3294.1588805,"p95":3405.046258583132,"p99":3414.8240850073203}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:49+00","p50":3238.037854,"p95":3462.270503521735,"p99":3477.741245552575}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:50+00","p50":3120.559551125,"p95":3541.536616185178,"p99":3785.2715815581155}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:51+00","p50":3419.03729325,"p95":3755.744896065429,"p99":4112.686248227599}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:52+00","p50":3251.309351,"p95":3908.312009622854,"p99":3970.708846166234}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:53+00","p50":3581.115248625,"p95":3790.956721882526,"p99":4131.5440587733665}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:54+00","p50":3535.80147,"p95":3757.565007969349,"p99":4136.431832956751}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:55+00","p50":3510.655485,"p95":3788.4992231799342,"p99":3822.2195938288555}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:56+00","p50":3439.595894,"p95":3940.5801276289153,"p99":4061.0985673719415}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:57+00","p50":3964.7582865,"p95":4034.958793415456,"p99":4043.927564961302}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:58+00","p50":3751.84411925,"p95":4060.4856320285735,"p99":4099.758436508774}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:30:59+00","p50":3816.466431,"p95":3938.3695194240718,"p99":4035.1222582758437}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:00+00","p50":3595.527729625,"p95":3806.0870691651985,"p99":3978.783428910409}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:01+00","p50":3529.123557875,"p95":3596.8950007888734,"p99":3775.67026030558}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:02+00","p50":3459.4602185,"p95":3610.389398705334,"p99":3753.6246790107034}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:03+00","p50":3448.1460242499998,"p95":3540.8655740641966,"p99":3781.168899077761}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:04+00","p50":3441.908457,"p95":3559.251966246547,"p99":3571.2337710813067}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:05+00","p50":3315.5394862499998,"p95":3545.6639289731556,"p99":3575.042187322312}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:06+00","p50":3279.6103425,"p95":3377.735875265194,"p99":3562.9161625489965}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:07+00","p50":3231.1080875000002,"p95":3322.3858001044364,"p99":3461.247460657553}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:08+00","p50":3315.588411375,"p95":3495.833273502558,"p99":3534.8468453574365}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:09+00","p50":3486.069855875,"p95":3655.721949059393,"p99":3671.6105747688343}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:10+00","p50":3466.056692,"p95":3768.529328379525,"p99":4123.4157893695}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:11+00","p50":3501.3364295,"p95":3663.3066266463347,"p99":3707.385709530411}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:12+00","p50":3496.8567631250003,"p95":3563.249472755375,"p99":3589.146102361638}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:13+00","p50":3534.144286375,"p95":3660.847700840688,"p99":3680.5917375579056}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:14+00","p50":3495.7922995000004,"p95":3677.873181481708,"p99":3995.3533549952085}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:15+00","p50":3582.5296097500004,"p95":3801.1565265428076,"p99":3860.5623522653104}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:16+00","p50":3539.8008715000005,"p95":3764.8101562714037,"p99":3983.31264432548}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:17+00","p50":3453.5803674999997,"p95":3549.017392868109,"p99":3583.840878299409}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:18+00","p50":3512.647535,"p95":3626.419556441483,"p99":3711.924317170323}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:19+00","p50":3569.39063075,"p95":3697.2365332264267,"p99":3809.4130768284376}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:20+00","p50":3709.5159999999996,"p95":3884.6497582782586,"p99":3988.4386778221406}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:21+00","p50":3953.7114432500002,"p95":4060.7064716712666,"p99":4251.2623390359}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:22+00","p50":3710.7905944999998,"p95":3919.5735547572576,"p99":4422.631094827162}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:23+00","p50":3271.4389674999998,"p95":3788.3852092551724,"p99":4160.729659864089}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:24+00","p50":3444.27061525,"p95":3737.232599894305,"p99":3770.5067512430073}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:25+00","p50":3679.79833325,"p95":3763.398821904205,"p99":3777.7754694434143}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:26+00","p50":3644.5340005000003,"p95":3845.744677675802,"p99":4286.156732732544}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:27+00","p50":3489.97961925,"p95":3756.511588477229,"p99":4166.009257308782}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:28+00","p50":3624.560219375,"p95":3869.9739798510873,"p99":3902.951383958582}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:29+00","p50":3692.81146975,"p95":3849.8759096310914,"p99":4143.107159167329}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:30+00","p50":3484.6042420000003,"p95":3723.634777649778,"p99":4069.1926659820656}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:31+00","p50":3431.370671125,"p95":3553.9647696356315,"p99":3764.312822415361}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:32+00","p50":3399.566164,"p95":3477.1480433717934,"p99":3648.9121210082}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:33+00","p50":3474.0090387500004,"p95":3569.555366527434,"p99":3575.9256553201544}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:34+00","p50":3428.359574,"p95":3541.672157725832,"p99":3870.0685503311743}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:35+00","p50":3401.8278682499995,"p95":3543.2718753308263,"p99":3802.2768286162923}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:36+00","p50":3356.0446819999997,"p95":3608.5555575196036,"p99":3981.245211466179}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:37+00","p50":3366.9547235,"p95":3592.9076917300604,"p99":3959.483071638911}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:38+00","p50":3261.3015159999995,"p95":3633.206571626271,"p99":3686.7556831387983}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:39+00","p50":3464.0775405000004,"p95":3713.420846888795,"p99":3727.056450500243}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:40+00","p50":3517.9295515000003,"p95":3804.1726187831127,"p99":4200.735564109886}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:41+00","p50":3397.3838057499997,"p95":3865.2684518221536,"p99":3890.6249622160417}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:42+00","p50":3719.437411,"p95":3813.377730369816,"p99":3895.800195486336}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:43+00","p50":3724.93072575,"p95":4037.8651544121494,"p99":4054.5078703446848}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:44+00","p50":3614.57812225,"p95":3796.6749806352273,"p99":3809.93960514431}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:45+00","p50":3592.7620420000003,"p95":3637.9392055471735,"p99":3658.233486002729}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:46+00","p50":3477.1631827499996,"p95":3631.308559628401,"p99":3757.57420874539}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:47+00","p50":3453.444891,"p95":3584.363133759086,"p99":3609.3309880817683}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:48+00","p50":3505.1170032500004,"p95":3699.674895320016,"p99":3722.475841638639}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:49+00","p50":3555.8795142500003,"p95":3660.062041917892,"p99":3909.487894982604}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:50+00","p50":3465.6183745,"p95":3577.227268444139,"p99":3786.7307982490097}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:51+00","p50":3446.625491,"p95":3572.3590277809267,"p99":3778.4374548907053}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:52+00","p50":3466.431602,"p95":3662.352001731951,"p99":3676.346612446866}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:53+00","p50":3518.72181525,"p95":3717.0133711133735,"p99":3997.2090863605695}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:54+00","p50":3474.96781825,"p95":3860.4010291300374,"p99":4263.328952180818}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:55+00","p50":3505.8102609999996,"p95":3883.484513173071,"p99":4201.915610193126}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:56+00","p50":3465.5266215,"p95":3860.1790328053935,"p99":3881.8627255511747}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:57+00","p50":3744.91052325,"p95":4003.364633434812,"p99":4019.4900447607115}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:58+00","p50":3796.92576625,"p95":4011.5291333987966,"p99":4225.7271347594915}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:31:59+00","p50":3903.5669395,"p95":4236.193413934033,"p99":4652.557186949406}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:00+00","p50":3866.2422245,"p95":4221.553308803587,"p99":4242.137830697063}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:01+00","p50":3785.0249162500004,"p95":3890.4978321676012,"p99":3902.5124180122416}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:02+00","p50":3758.9642918749996,"p95":3891.082319473586,"p99":4093.4488109554263}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:03+00","p50":3521.07453925,"p95":3894.0867914494174,"p99":3938.154887690421}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:04+00","p50":3534.7569875,"p95":3666.813729741283,"p99":3777.841281102767}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:05+00","p50":3442.7488135,"p95":3616.7597670142763,"p99":3640.8398029213936}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:06+00","p50":3386.0516129999996,"p95":3521.2379296341287,"p99":3545.7774196748483}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:07+00","p50":3244.7721902499998,"p95":3479.134181019274,"p99":3785.669952218444}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:08+00","p50":3307.974885,"p95":3469.9580510300584,"p99":3772.7176282428754}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:09+00","p50":3430.71233875,"p95":3496.6736464078062,"p99":3519.699998936084}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:10+00","p50":3373.443238,"p95":3419.2523624974992,"p99":3464.795967616987}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:11+00","p50":3401.5647,"p95":3501.5934502292084,"p99":3647.302945251589}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:12+00","p50":3347.2917808750003,"p95":3499.152017466597,"p99":3690.3551592295134}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:13+00","p50":3320.226178,"p95":3483.6161599305137,"p99":3538.9560711981867}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:14+00","p50":3338.1117007500006,"p95":3398.798204759887,"p99":3567.6067428150454}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:15+00","p50":3362.8648415000002,"p95":3435.320791236668,"p99":3507.3407142000524}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:16+00","p50":3375.1364482500003,"p95":3453.481788658637,"p99":3682.2988259022977}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:17+00","p50":3392.158482,"p95":3493.5732107254676,"p99":3664.6853338944015}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:18+00","p50":3393.5506809999997,"p95":3496.129845815709,"p99":3762.1872126109765}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:19+00","p50":3431.746792875,"p95":3557.922592347926,"p99":3682.464610230809}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:20+00","p50":3417.937324375,"p95":3605.833217991676,"p99":3644.008650568503}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:21+00","p50":3536.157504,"p95":3652.6566469649542,"p99":3668.9767202921103}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:22+00","p50":3573.964334625,"p95":3716.6748508281885,"p99":3736.300000667183}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:23+00","p50":3574.5106554999998,"p95":3731.983450501681,"p99":3985.6675754038906}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:24+00","p50":3561.676681875,"p95":3715.35930854849,"p99":3788.7033266959315}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:25+00","p50":3533.6907845,"p95":3594.8859456138534,"p99":3714.3938559417775}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:26+00","p50":3593.0364145,"p95":3657.6141942203417,"p99":3830.1989303412834}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:27+00","p50":3661.7067892500004,"p95":3743.8844198051056,"p99":3795.778162143844}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:28+00","p50":3631.8086755000004,"p95":3711.6981926752173,"p99":3938.8334347333416}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:29+00","p50":3689.95221575,"p95":3823.830609582662,"p99":4020.9464967347417}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:30+00","p50":3562.264131125,"p95":3805.0882506026583,"p99":4186.573419924836}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:31+00","p50":3564.1375825,"p95":3712.882831074653,"p99":4056.7334060136336}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:32+00","p50":3128.633622375,"p95":3649.8465030868415,"p99":4136.804781306533}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:33+00","p50":3450.3158605,"p95":3714.8673924076634,"p99":3740.2094115415343}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:34+00","p50":3319.3968370000002,"p95":3804.877456581749,"p99":4219.933791027477}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:35+00","p50":2944.5473735,"p95":3780.756769263538,"p99":4226.689426627014}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:36+00","p50":3120.2606285,"p95":3781.365760582856,"p99":4256.4855839021375}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:37+00","p50":2823.074562625,"p95":3843.7532884222182,"p99":4392.923315029836}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:38+00","p50":3042.249261625,"p95":3786.952496732005,"p99":3821.563042336723}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:39+00","p50":3220.976807625,"p95":3694.019552858429,"p99":3709.3114313027068}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:40+00","p50":3369.754670625,"p95":3784.6569635414676,"p99":3802.442111447718}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:41+00","p50":3336.118755,"p95":3764.778414296204,"p99":3780.2896401479907}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:42+00","p50":3722.176022,"p95":3808.7652939955824,"p99":4380.597250415075}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:43+00","p50":3516.259387875,"p95":3838.997130840373,"p99":4288.207714733399}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:44+00","p50":3323.125051,"p95":3906.375988443732,"p99":3916.617406272667}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:45+00","p50":3582.16289875,"p95":3969.3560087238334,"p99":3986.1039275690496}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:46+00","p50":3672.3183955,"p95":3936.3522043676644,"p99":3970.506875884468}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:47+00","p50":3250.85904675,"p95":3664.4440377957644,"p99":3719.851395948471}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:48+00","p50":3182.9208418750004,"p95":3699.2565400205863,"p99":4177.297578115381}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:49+00","p50":3133.6916442499996,"p95":3794.8582067803154,"p99":3810.9802899117867}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:50+00","p50":3435.29013825,"p95":3818.442334332746,"p99":3840.4072509542907}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:51+00","p50":3128.7917066249997,"p95":3728.509411692336,"p99":4254.926254205663}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:52+00","p50":3382.47989925,"p95":3688.6050501236655,"p99":3701.0876761768536}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:53+00","p50":3150.4898811250005,"p95":3646.4036809987097,"p99":3660.2521585525005}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:54+00","p50":3627.655869,"p95":3709.3590389193364,"p99":3719.6012356596666}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:55+00","p50":3135.35847125,"p95":3693.1751886868988,"p99":4217.087671904136}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:56+00","p50":3352.04896275,"p95":3929.872607445457,"p99":4235.138431324108}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:57+00","p50":3283.700826125,"p95":4010.666994468792,"p99":4558.484819922387}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:58+00","p50":3553.52013,"p95":4081.0045083422315,"p99":4589.51986888967}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:32:59+00","p50":3334.742819,"p95":4143.573513818559,"p99":4178.239681187713}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:00+00","p50":3169.845703,"p95":3910.599568503994,"p99":3932.887344870285}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:01+00","p50":3603.193767875,"p95":3829.8509433774716,"p99":3857.0272200790755}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:02+00","p50":3296.13759475,"p95":3677.8975183841612,"p99":4003.0082304314833}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:03+00","p50":3459.224407,"p95":3782.5509582433137,"p99":3828.5162747043273}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:04+00","p50":3181.174402125,"p95":3810.702508908106,"p99":3815.94213437593}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:05+00","p50":3373.8563655,"p95":3567.42585662907,"p99":3885.1159026606524}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:06+00","p50":3422.3456665,"p95":3536.356835766133,"p99":3775.7492037349753}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:07+00","p50":3256.576791,"p95":3487.4643919902683,"p99":3501.5305670282237}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:08+00","p50":3318.7573675000003,"p95":3439.844288698298,"p99":3495.25823157192}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:09+00","p50":3450.593443875,"p95":3514.483453841459,"p99":3616.8969896875465}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:10+00","p50":3486.5128581249996,"p95":3555.098009864056,"p99":3737.0514720260016}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:11+00","p50":3510.80882825,"p95":3719.4478919943213,"p99":3734.541509477482}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:12+00","p50":3487.6408197499995,"p95":3609.913571098519,"p99":3788.495221211842}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:13+00","p50":3408.991982,"p95":3699.4698832591876,"p99":3720.6972481383386}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:14+00","p50":3435.1895895,"p95":3653.1945228326376,"p99":3678.995999765276}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:15+00","p50":3285.571557,"p95":3599.9421596368206,"p99":3907.379901930664}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:16+00","p50":3310.7768505,"p95":3506.634577186947,"p99":3780.5026551115466}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:17+00","p50":3365.386935,"p95":3431.9658334374976,"p99":3717.7845283201877}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:18+00","p50":3401.534108,"p95":3560.1482543385137,"p99":3569.386819855467}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:19+00","p50":3317.86118325,"p95":3496.6544383508744,"p99":3584.083719152192}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:20+00","p50":3366.984532,"p95":3500.5743978512482,"p99":3581.534978859359}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:21+00","p50":3483.024207,"p95":3721.803145832037,"p99":3749.3547942120026}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:22+00","p50":3475.3321455,"p95":3825.7781628080024,"p99":3846.695277707636}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:23+00","p50":3486.9305535000003,"p95":3887.078975629277,"p99":4180.624671783922}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:24+00","p50":3570.225522625,"p95":3802.7040695135292,"p99":3829.8164213212285}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:25+00","p50":3441.4975079999995,"p95":3682.071365806855,"p99":3747.138209107874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:26+00","p50":3494.5638233749996,"p95":3585.3730735424897,"p99":3615.8751099078213}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:27+00","p50":3652.2655885,"p95":3818.7296058712673,"p99":3860.8217181143978}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:28+00","p50":3750.688881625,"p95":3874.437104268433,"p99":3918.1032279435994}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:29+00","p50":3717.844779125,"p95":3902.1546430818,"p99":3916.876487522555}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:30+00","p50":3589.76033,"p95":3714.682854480323,"p99":3740.1095093504173}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:31+00","p50":3439.5763081249997,"p95":3585.137966265356,"p99":3604.445266157197}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:32+00","p50":3238.9940975,"p95":3318.4854029957196,"p99":3459.952173655004}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:33+00","p50":3238.48884575,"p95":3382.609105320302,"p99":3391.6435358573262}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:34+00","p50":3289.1457518750003,"p95":3403.866535265885,"p99":3422.370172174908}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:35+00","p50":3373.0293977499996,"p95":3468.978301383829,"p99":3496.1470220042534}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:36+00","p50":3398.71280225,"p95":3481.3213507281926,"p99":3710.568083513491}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:37+00","p50":3337.5059915,"p95":3438.8096290214353,"p99":3699.0975993127445}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:38+00","p50":3361.140047,"p95":3471.5194830361115,"p99":3485.528462453617}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:39+00","p50":3415.2017774999995,"p95":3514.0802160646285,"p99":3631.0059867173563}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:40+00","p50":3392.1880770000002,"p95":3510.6403302173167,"p99":3640.018457406863}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:41+00","p50":3419.04169425,"p95":3536.307513721518,"p99":3585.924965776796}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:42+00","p50":3453.4179835000004,"p95":3588.3057418158783,"p99":3636.078642135562}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:43+00","p50":3465.7024305,"p95":3652.082137315835,"p99":3828.1042652892306}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:44+00","p50":3453.852097625,"p95":3675.532428883128,"p99":4053.0969662902407}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:45+00","p50":3571.3691145,"p95":3809.42849433039,"p99":4105.94041180805}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:46+00","p50":3412.691025875,"p95":3653.259681458742,"p99":4104.278309637808}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:47+00","p50":3139.555128375,"p95":3705.0516382075284,"p99":3728.34230787269}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:48+00","p50":3508.997365875,"p95":3925.499289660099,"p99":4418.853954400787}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:49+00","p50":3073.2853862499996,"p95":4047.9358928198694,"p99":4053.5925690044774}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:50+00","p50":3589.0081845,"p95":3945.8495593514194,"p99":4364.499861497526}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:51+00","p50":3378.3101535,"p95":3810.3323565843775,"p99":4172.079744715696}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:52+00","p50":3714.5271836250004,"p95":3804.4141861421704,"p99":3894.2217535681325}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:53+00","p50":3590.1204912499998,"p95":3859.479061009706,"p99":3897.9833658660095}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:54+00","p50":3640.71536225,"p95":4030.958822130475,"p99":4071.9483684118995}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:55+00","p50":3595.2649544999995,"p95":3925.698571643412,"p99":3964.574776756694}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:56+00","p50":3636.7491794999996,"p95":4098.875573206085,"p99":4218.341152733856}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:57+00","p50":3832.6566256249994,"p95":4220.16231920858,"p99":4230.801906713411}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:58+00","p50":3942.29162025,"p95":4233.556781615703,"p99":4256.242926476487}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:33:59+00","p50":3746.3463176249998,"p95":4127.1696516030015,"p99":4150.963059169812}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:00+00","p50":3498.4709141250005,"p95":3799.6492082487302,"p99":4135.346375100506}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:01+00","p50":3387.635681,"p95":3670.306668527923,"p99":3997.154780428478}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:02+00","p50":3369.58838675,"p95":3681.7336391333556,"p99":3973.481205507835}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:03+00","p50":3465.5286841250004,"p95":3592.1394821921795,"p99":3852.6757242131926}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:04+00","p50":3394.46818,"p95":3724.833749662024,"p99":3741.1003799758764}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:05+00","p50":3402.86243975,"p95":3657.813625105696,"p99":3677.7878723854406}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:06+00","p50":3163.8792205,"p95":3508.058427448469,"p99":3523.7274278999757}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:07+00","p50":3292.808589,"p95":3572.5512119777345,"p99":3878.1894401108852}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:08+00","p50":3446.0996219999997,"p95":3600.633204961879,"p99":4033.468484689549}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:09+00","p50":3130.749758,"p95":3728.832261129825,"p99":3757.5166109964375}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:10+00","p50":3047.279666375,"p95":3693.335244332643,"p99":3705.1153208791457}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:11+00","p50":2895.0548934999997,"p95":3831.397916103867,"p99":4248.0251215174285}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:12+00","p50":3077.4701772500002,"p95":3919.303175274477,"p99":3936.537272233346}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:13+00","p50":3344.3534405,"p95":3787.0198566583163,"p99":3815.8968901866056}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:14+00","p50":2895.2348385,"p95":3862.1262132420293,"p99":3869.5771502414777}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:15+00","p50":3137.24550675,"p95":4006.012545503318,"p99":4029.7102260569095}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:16+00","p50":3157.3042127500003,"p95":4043.3047110715133,"p99":4645.607724830857}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:17+00","p50":2975.6361687500003,"p95":3958.508975383385,"p99":4039.30628459469}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:18+00","p50":3045.40052975,"p95":3756.470744663894,"p99":4175.04317967389}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:19+00","p50":3235.589908,"p95":3775.337360887122,"p99":3789.035407948196}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:20+00","p50":3487.8200465,"p95":3763.7435045225548,"p99":3996.8598591598625}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:21+00","p50":2872.82978,"p95":3608.953226035885,"p99":3635.1179246450556}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:22+00","p50":2958.054996,"p95":3736.574047639511,"p99":4161.682315335826}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:23+00","p50":3006.6217956250002,"p95":3857.237735933179,"p99":4255.984449768601}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:24+00","p50":3023.9294515,"p95":3832.5441704648265,"p99":3842.1441582669454}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:25+00","p50":3271.3057325,"p95":3681.1146052445197,"p99":3697.7556678332685}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:26+00","p50":3569.3926116250004,"p95":3831.214920328634,"p99":4059.7252070504846}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:27+00","p50":3805.7009657500003,"p95":3939.1264691056904,"p99":4430.315330790864}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:28+00","p50":3573.9586185,"p95":3920.1418681097502,"p99":4377.824044301384}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:29+00","p50":3616.773468625,"p95":3982.8224535779586,"p99":3997.901012850756}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:30+00","p50":3088.4700205,"p95":3927.2577546375087,"p99":3948.6822346107733}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:31+00","p50":3045.3167595,"p95":3923.2711699199426,"p99":4263.6889212438355}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:32+00","p50":3328.53628475,"p95":3729.821422582563,"p99":4294.672040020914}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:33+00","p50":3239.2317125,"p95":3760.11747951899,"p99":3774.0631656093415}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:34+00","p50":3082.1803685,"p95":3623.1260075410905,"p99":4008.3018890664043}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:35+00","p50":3242.9386825,"p95":3631.3762066817008,"p99":3970.287846898337}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:36+00","p50":3216.82392425,"p95":3542.6372896987536,"p99":3900.082852718513}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:37+00","p50":3321.6866977500003,"p95":3631.3614929351234,"p99":4179.763123978745}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:38+00","p50":3441.01816675,"p95":3684.423606924205,"p99":4191.793054711691}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:39+00","p50":3291.204309875,"p95":3762.5782005241936,"p99":3778.7736776766387}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:40+00","p50":3163.3553192499994,"p95":3861.0219577743164,"p99":4339.892931641085}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:41+00","p50":3244.8863195,"p95":3927.7693669101864,"p99":4390.132135672763}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:42+00","p50":3630.9575205,"p95":3881.573138799473,"p99":3890.6161695319365}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:43+00","p50":3299.4644264999997,"p95":3838.5361242122704,"p99":4376.705555134912}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:44+00","p50":3205.96124725,"p95":3838.4422345695752,"p99":3856.1658884998515}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:45+00","p50":3548.628088875,"p95":3849.4272964141073,"p99":4196.366660999596}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:46+00","p50":3244.4499715,"p95":3717.003389741006,"p99":3736.448258476208}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:47+00","p50":3347.972424875,"p95":3563.066944906756,"p99":3673.9512113340074}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:48+00","p50":3014.55483625,"p95":3919.2049229816726,"p99":4080.780933412855}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:49+00","p50":3003.4541335000004,"p95":3871.453055649346,"p99":4445.613718286867}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:50+00","p50":2709.3514495,"p95":4015.558022775827,"p99":4042.361113243641}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:51+00","p50":2898.7944515,"p95":4196.782348359566,"p99":4294.913863092732}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:52+00","p50":3586.4098012500003,"p95":4314.794977916504,"p99":5066.693310374195}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:53+00","p50":3878.4684231250003,"p95":4366.639585615816,"p99":4998.250267870642}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:54+00","p50":2985.559950125,"p95":4146.514661922689,"p99":4721.983108290984}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:55+00","p50":3514.794834875,"p95":4060.4844006566514,"p99":4723.053596403144}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:56+00","p50":3441.0566635,"p95":4040.575391883749,"p99":4827.496922428762}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:57+00","p50":3594.412255,"p95":4173.209984524019,"p99":4800.108597315587}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:58+00","p50":3643.8599934999997,"p95":4182.418683369246,"p99":4819.785550610762}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:34:59+00","p50":3462.713884,"p95":4215.99838017416,"p99":4247.683291915671}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:00+00","p50":3581.533324125,"p95":4220.620022711296,"p99":4835.847875721057}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:01+00","p50":2978.554839,"p95":4028.777248154268,"p99":4039.651702202888}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:02+00","p50":3081.9187899999997,"p95":3677.977268134901,"p99":4187.099158215424}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:03+00","p50":3123.8352075000003,"p95":3608.7266939071615,"p99":3625.8004172927303}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:04+00","p50":3098.161916375,"p95":3505.2857797660954,"p99":3838.1944094590417}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:05+00","p50":3247.5487818750003,"p95":3472.8902166588846,"p99":3839.6098492052106}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:06+00","p50":3221.60484275,"p95":3522.4409199708393,"p99":3558.215986169553}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:07+00","p50":3332.892356375,"p95":3544.7647355889267,"p99":3564.422538586498}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:08+00","p50":3206.3131087499996,"p95":3611.011073151858,"p99":3972.3538859805954}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:09+00","p50":3380.98568625,"p95":3788.5150501860408,"p99":4195.960391280133}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:10+00","p50":3422.2420975,"p95":3764.6850499641478,"p99":3783.112280381194}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:11+00","p50":3513.9147315,"p95":4006.9943866679923,"p99":4336.4228121136675}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:12+00","p50":3323.951467125,"p95":4002.1166629718723,"p99":4065.3922929927094}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:13+00","p50":3156.563318,"p95":3539.998817735312,"p99":3565.4417408422987}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:14+00","p50":3243.9060325,"p95":3564.597529212912,"p99":3582.2080575864757}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:15+00","p50":3332.468858,"p95":3562.8805880971504,"p99":3919.403382798032}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:16+00","p50":3329.46508875,"p95":3471.388515586339,"p99":3484.379688169521}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:17+00","p50":3340.324231625,"p95":3509.1708794131587,"p99":3792.716277281509}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:18+00","p50":3386.2363745,"p95":3641.33248375619,"p99":3990.6066515840766}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:19+00","p50":3325.373976,"p95":3661.3864611270083,"p99":4069.4420752670108}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:20+00","p50":3061.67817475,"p95":3833.7152084076365,"p99":4201.373549628639}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:21+00","p50":3349.413043,"p95":3994.21333914843,"p99":4066.066548474357}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:22+00","p50":3727.3894725,"p95":4062.260070456264,"p99":4670.249001871408}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:23+00","p50":3618.499000875,"p95":4000.1313146958296,"p99":4613.663863972214}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:24+00","p50":3693.8388905,"p95":3851.9244007586594,"p99":3871.2940872333324}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:25+00","p50":3593.6448689999997,"p95":3703.132649433184,"p99":4150.020248619889}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:26+00","p50":2988.3706119999997,"p95":3870.147566990106,"p99":4342.548978875222}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:27+00","p50":3454.633933625,"p95":3836.3529217637274,"p99":3869.7770473680066}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:28+00","p50":3251.48157875,"p95":4046.8441158452165,"p99":4077.5269508248293}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:29+00","p50":3103.2339469999997,"p95":4121.182401381578,"p99":4755.378685531502}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:30+00","p50":3153.14558275,"p95":4254.8491418279555,"p99":4881.620436325295}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:31+00","p50":3149.2232702499996,"p95":4287.569354463049,"p99":4933.370073345574}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:32+00","p50":2724.96798425,"p95":4189.507533076027,"p99":4993.513635400879}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:33+00","p50":4045.248184,"p95":4142.8334880475495,"p99":4847.839516417107}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:34+00","p50":3485.281145375,"p95":4042.955970896937,"p99":4085.4816387048954}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:35+00","p50":2875.19192425,"p95":3799.528869651587,"p99":4509.36267402633}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:36+00","p50":2868.012462125,"p95":3737.680846759628,"p99":4394.346155229578}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:37+00","p50":2879.0128987499997,"p95":3798.759790751284,"p99":4367.761247846258}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:38+00","p50":3127.5796870000004,"p95":4046.8442360350978,"p99":4159.094469902101}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:39+00","p50":3271.86984225,"p95":4187.118308119025,"p99":4201.300334547806}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:40+00","p50":4094.5056495,"p95":4251.834706464559,"p99":4288.236610655692}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:41+00","p50":2796.965528375,"p95":4211.246564329757,"p99":4223.440536221747}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:42+00","p50":3257.427243,"p95":4185.788253451738,"p99":4277.623178248362}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:43+00","p50":2899.022757875,"p95":4205.447417900844,"p99":5148.998891024335}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:44+00","p50":2943.101834125,"p95":4279.618092531202,"p99":4934.406544976412}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:45+00","p50":3168.23512975,"p95":4154.2314415425935,"p99":4173.758699749813}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:46+00","p50":2989.86978275,"p95":4007.9395055709388,"p99":4842.0584035592565}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:47+00","p50":3053.9327605,"p95":3987.643448863165,"p99":4544.025916972678}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:48+00","p50":2710.7414845000003,"p95":3849.8432113019257,"p99":4594.0588548568085}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:49+00","p50":2449.783796,"p95":3969.3619884800482,"p99":4469.254321870878}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:50+00","p50":3729.743166,"p95":3837.8830170567303,"p99":4401.523413081409}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:51+00","p50":2791.55880375,"p95":3997.590328892255,"p99":4032.1172588712407}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:52+00","p50":2936.9422914999996,"p95":4171.841213494693,"p99":4875.892768995115}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:53+00","p50":2606.84871,"p95":4136.207909939127,"p99":4944.889800747749}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:54+00","p50":2436.3920346249997,"p95":4288.684151742864,"p99":4312.697367951695}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:55+00","p50":3507.0176678750004,"p95":4181.958056209514,"p99":4187.018088256304}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:56+00","p50":3365.8077505,"p95":4164.439919996855,"p99":5069.101946116794}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:57+00","p50":2885.780237125,"p95":4285.143474876769,"p99":4308.3129185469525}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:58+00","p50":4256.731619,"p95":4437.367963113219,"p99":4650.908735270181}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:35:59+00","p50":4363.884933125,"p95":4495.397259939697,"p99":4533.903431603031}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:00+00","p50":3743.86408675,"p95":4491.040267649324,"p99":4506.070929760761}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:01+00","p50":3584.18721075,"p95":4396.721702161347,"p99":5273.136318810753}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:02+00","p50":3217.92656275,"p95":4325.885033630558,"p99":5131.768819336363}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:03+00","p50":3510.06725875,"p95":4052.8319933864427,"p99":4070.528229887291}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:04+00","p50":2856.133388,"p95":3863.7332796447963,"p99":3889.027556136864}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:05+00","p50":3493.41588475,"p95":3851.1014169327786,"p99":3867.1428557453946}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:06+00","p50":2716.0430349999997,"p95":3761.1377531640774,"p99":4267.714604344921}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:07+00","p50":3484.8959501249997,"p95":3852.3930410618705,"p99":4409.458496364488}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:08+00","p50":3090.3660527499997,"p95":3852.351020718823,"p99":3881.010974218695}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:09+00","p50":3373.49573125,"p95":3915.924333157908,"p99":3921.20069522395}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:10+00","p50":2699.166351,"p95":3966.627115187172,"p99":3991.6913599887243}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:11+00","p50":3455.77741875,"p95":3922.6459076577703,"p99":4572.732310940787}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:12+00","p50":3088.0228740000002,"p95":3889.4824392218034,"p99":3911.7785883900206}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:13+00","p50":3138.3214425,"p95":3836.377179447394,"p99":4320.477571211984}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:14+00","p50":3249.4243725,"p95":3772.2683258124503,"p99":4228.23048817292}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:15+00","p50":2952.891978875,"p95":3885.5557352936175,"p99":4519.173384409962}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:16+00","p50":2587.3103604999997,"p95":3871.336470745887,"p99":4501.041078296973}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:17+00","p50":3778.3894604999996,"p95":3880.665311470103,"p99":4459.968642643756}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:18+00","p50":3043.8382375,"p95":3827.5836657410814,"p99":4510.530524562578}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:19+00","p50":3459.682346,"p95":3994.897163330377,"p99":4038.0609974526906}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:20+00","p50":3519.302135625,"p95":4170.32792269428,"p99":4574.06059026438}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:21+00","p50":3046.61098,"p95":4568.81273279418,"p99":4758.246127762275}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:22+00","p50":4544.923097,"p95":4728.2395503668195,"p99":4951.403425014146}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:23+00","p50":3583.7264538749996,"p95":4746.395356841469,"p99":5591.490629165487}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:24+00","p50":3915.49382075,"p95":4723.654149367353,"p99":5499.290067641081}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:25+00","p50":4100.2323215,"p95":4802.911764469886,"p99":5507.375288059913}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:26+00","p50":3828.7071482499996,"p95":4639.1830049797,"p99":4812.431778594844}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:27+00","p50":2996.68651975,"p95":4501.015114459288,"p99":4514.674606690127}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:28+00","p50":3815.3495607500004,"p95":4498.395042493334,"p99":4572.881378172114}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:29+00","p50":3921.7775144999996,"p95":4429.037332488706,"p99":4449.555745536676}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:30+00","p50":3001.9882165,"p95":4285.531867268633,"p99":5033.9110607195025}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:31+00","p50":3465.83495625,"p95":4163.766026750244,"p99":4182.800838659262}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:32+00","p50":3007.6576605,"p95":4234.986313460867,"p99":4280.000996415401}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:33+00","p50":2985.1163005,"p95":4061.931901175288,"p99":4089.658088398897}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:34+00","p50":3294.210603,"p95":4209.124619817751,"p99":4274.668200468254}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:35+00","p50":3403.63569475,"p95":4194.661734274166,"p99":4209.279828245951}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:36+00","p50":3152.6921466249996,"p95":4162.849474730015,"p99":4784.621516238401}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:37+00","p50":3507.047877125,"p95":4037.5742459027547,"p99":4744.835613664199}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:38+00","p50":2610.10875725,"p95":3941.609704220851,"p99":3984.65390175123}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:39+00","p50":2628.5494826249997,"p95":4004.6741257941453,"p99":4023.183496714831}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:40+00","p50":4011.4232792499997,"p95":4137.581380399988,"p99":4862.819925475552}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:41+00","p50":2966.74192375,"p95":4063.4918259997216,"p99":4097.921082560894}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:42+00","p50":2818.21877,"p95":4057.4443107481698,"p99":4454.599374572968}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:43+00","p50":3602.70014275,"p95":4377.072418103921,"p99":5016.234428872784}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:44+00","p50":3892.0824121250002,"p95":4513.518403554581,"p99":4527.941811616564}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:45+00","p50":3986.424561,"p95":4206.337510220834,"p99":4963.738925843397}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:46+00","p50":3098.9148667500003,"p95":4050.2340626847968,"p99":4680.436949755075}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:47+00","p50":3658.855395,"p95":3778.784719130134,"p99":3841.588407207383}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:48+00","p50":3341.204683,"p95":3842.333833451337,"p99":3877.5041547425844}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:49+00","p50":3181.657119375,"p95":3824.1748138695834,"p99":4217.725168225138}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:50+00","p50":3315.5030049999996,"p95":3731.1625229048045,"p99":3739.605685617836}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:51+00","p50":3510.9966679999998,"p95":3798.6342243246218,"p99":4307.55341299712}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:52+00","p50":2891.8622851249997,"p95":3844.970035729626,"p99":4504.592750609978}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:53+00","p50":2915.4537805,"p95":3808.3020851560173,"p99":4271.594438712463}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:54+00","p50":3824.31375725,"p95":4013.7693087051175,"p99":4020.067913292537}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:55+00","p50":3484.947964625,"p95":3894.0324584102705,"p99":3913.9433264825543}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:56+00","p50":3267.6339765,"p95":3916.5135917322036,"p99":3996.471984863067}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:57+00","p50":3797.8761508750003,"p95":3944.961638856124,"p99":4462.338404996447}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:58+00","p50":3643.94262175,"p95":3895.748917291327,"p99":4322.826861763663}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:36:59+00","p50":3568.0283475,"p95":3886.9000719406713,"p99":3902.0362698401527}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:00+00","p50":3310.1594455,"p95":3769.773554140369,"p99":3799.076792843233}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:01+00","p50":3525.09342525,"p95":3744.4342542004783,"p99":3762.4940408138823}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:02+00","p50":3245.278846,"p95":3595.907444316883,"p99":3864.9538689235765}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:03+00","p50":3314.75643475,"p95":3494.555932669686,"p99":3746.11736535495}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:04+00","p50":3353.624390375,"p95":3472.977663565533,"p99":3485.053819011269}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:05+00","p50":3422.7164685000002,"p95":3570.893219608491,"p99":3581.3059614697577}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:06+00","p50":3368.88378225,"p95":3462.557147885274,"p99":3716.3456514636982}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:07+00","p50":3309.1528705,"p95":3419.606062845201,"p99":3567.130206384319}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:08+00","p50":3406.9794566250002,"p95":3493.388559922437,"p99":3651.2075566625863}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:09+00","p50":3408.0434215,"p95":3661.5526306014804,"p99":3700.305764202222}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:10+00","p50":3413.3691772500006,"p95":3740.820677432601,"p99":3755.801216690009}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:11+00","p50":3415.7365255,"p95":3750.9445179614986,"p99":4061.894562442909}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:12+00","p50":3278.8301031250003,"p95":3755.007047475542,"p99":4206.426245956215}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:13+00","p50":3402.63676175,"p95":3693.296492758716,"p99":4116.672353912626}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:14+00","p50":3557.029718,"p95":3647.925557060739,"p99":4073.4530057835686}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:15+00","p50":3253.748345,"p95":3684.6718938621484,"p99":4108.638721789612}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:16+00","p50":3419.0436036250003,"p95":3585.2806686682266,"p99":4001.8634271006995}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:17+00","p50":3434.43373225,"p95":3582.681483585099,"p99":3615.66662480117}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:18+00","p50":3391.859585,"p95":3731.6136584789815,"p99":3824.8229743764305}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:19+00","p50":3411.553024375,"p95":3790.274178662686,"p99":3842.7109842697214}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:20+00","p50":3223.6720425,"p95":3743.099909651252,"p99":3761.4501949842834}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:21+00","p50":3356.9126255,"p95":3857.7868814780563,"p99":3872.089933161051}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:22+00","p50":3598.82665,"p95":3854.8925191054286,"p99":4243.080412980745}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:23+00","p50":3453.4808617500003,"p95":3682.9760586422813,"p99":3989.053204131278}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:24+00","p50":3427.2030139999997,"p95":3659.4922971352375,"p99":3992.6813255381544}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:25+00","p50":3318.3027522499997,"p95":3632.9915102109335,"p99":4042.552549474309}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:26+00","p50":3547.6308885,"p95":3719.180060545724,"p99":4042.975530867754}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:27+00","p50":3625.658390875,"p95":3824.383943833761,"p99":4195.353800682333}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:28+00","p50":3642.36177425,"p95":4006.2002272978175,"p99":4106.644754985203}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:29+00","p50":3559.284158875,"p95":4234.598160885796,"p99":4321.494290144283}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:30+00","p50":3848.686694875,"p95":4174.32019345482,"p99":4723.710214249359}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:31+00","p50":3576.12583025,"p95":3988.9525108093735,"p99":4366.683976785913}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:32+00","p50":3455.2719765,"p95":3583.0244307004964,"p99":3618.178509028993}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:33+00","p50":3291.9913704999994,"p95":3564.87602276459,"p99":3574.7378226029505}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:34+00","p50":3298.2708615,"p95":3570.6752477295377,"p99":3890.457578465164}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:35+00","p50":3401.4914325,"p95":3552.6463180810656,"p99":3561.688973906151}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:36+00","p50":3457.4994845,"p95":3540.5123513418785,"p99":3853.413171264503}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:37+00","p50":3363.1095045,"p95":3495.061416502095,"p99":3609.6269487665745}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:38+00","p50":3341.153424,"p95":3466.1791879105776,"p99":3572.0251220978116}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:39+00","p50":3473.6677044999997,"p95":3585.2052601565174,"p99":3671.6112879612083}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:40+00","p50":3519.229607625,"p95":3621.881969243256,"p99":3688.9951363416353}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:41+00","p50":3598.8833355,"p95":3710.523609652865,"p99":3946.453132010855}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:42+00","p50":3488.9927213749997,"p95":3618.104353424436,"p99":3845.289453476398}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:43+00","p50":3613.0980995,"p95":3680.036977898419,"p99":3702.9321229289762}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:44+00","p50":3327.0913025,"p95":3635.3052172134735,"p99":3663.3976563715055}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:45+00","p50":3331.8993035000003,"p95":3782.4267852145845,"p99":3830.87116348375}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:46+00","p50":3228.1868535,"p95":3894.5397181503886,"p99":4359.5418780791615}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:47+00","p50":3525.51671275,"p95":3772.641932376616,"p99":3824.0038979545207}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:48+00","p50":3193.45321825,"p95":3685.4780006875103,"p99":3736.2243719210664}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:49+00","p50":3298.8055,"p95":3784.7194713752833,"p99":3816.0418820456484}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:50+00","p50":3471.512554375,"p95":3925.5184019037197,"p99":3993.8975699192447}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:51+00","p50":3561.571171,"p95":4001.274763204815,"p99":4441.969853491452}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:52+00","p50":3934.336734,"p95":4037.320494333948,"p99":4391.926628363157}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:53+00","p50":3680.80142275,"p95":4078.4014689957276,"p99":4428.02969630269}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:54+00","p50":3390.3042051250004,"p95":4172.626428921715,"p99":4750.926462032977}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:55+00","p50":3353.9148342500002,"p95":4111.557019303195,"p99":4870.194358258963}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:56+00","p50":3658.81108975,"p95":4019.8119669859643,"p99":4727.758418822964}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:57+00","p50":3359.839144,"p95":4142.10730159189,"p99":4204.798398071085}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:58+00","p50":3663.0039724999997,"p95":4224.443136671066,"p99":4321.2610536334405}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:37:59+00","p50":3444.798289,"p95":4372.122585808101,"p99":4384.450370127855}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:00+00","p50":3692.54183675,"p95":4370.0028406506435,"p99":4384.369657872809}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:01+00","p50":3815.4202207499998,"p95":4292.460770433715,"p99":5190.8497116872795}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:02+00","p50":3373.564365,"p95":4355.944823094053,"p99":5147.175165880001}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:03+00","p50":3800.07844775,"p95":4123.3412069030655,"p99":4155.8316191476815}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:04+00","p50":3249.32038375,"p95":4075.8401603311963,"p99":4135.663831702366}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:05+00","p50":3528.032972375,"p95":4224.238507238596,"p99":4233.703857355617}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:06+00","p50":2772.0677638750003,"p95":4356.552230304373,"p99":4379.176684694948}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:07+00","p50":3426.8167030000004,"p95":4364.667035385944,"p99":5027.823369201743}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:08+00","p50":3879.9142635,"p95":4115.143374730681,"p99":4184.5415026945675}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:09+00","p50":3356.9741482500003,"p95":4132.926249281935,"p99":4977.609855920949}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:10+00","p50":2975.7883564999997,"p95":4154.640118212348,"p99":4234.36893432009}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:11+00","p50":3490.091174125,"p95":4237.150163221894,"p99":4879.008794504289}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:12+00","p50":4019.5821017499998,"p95":4266.734180283428,"p99":4899.903778477794}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:13+00","p50":3100.2930914999997,"p95":4325.4929907744845,"p99":4335.737727552591}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:14+00","p50":4136.2015685,"p95":4251.0994588254935,"p99":4870.111672883472}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:15+00","p50":3826.5988255,"p95":4101.882226951302,"p99":4145.766233050393}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:16+00","p50":3744.57932425,"p95":3967.8171092914945,"p99":4022.7745110949163}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:17+00","p50":3125.37451975,"p95":3828.683149614901,"p99":3849.0386225563707}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:18+00","p50":3704.4241497499997,"p95":3807.8204659737066,"p99":3822.7443517954243}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:19+00","p50":3602.304842,"p95":3729.054639908333,"p99":3738.470980868858}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:20+00","p50":3336.064748125,"p95":3527.3078206951964,"p99":3682.957971140092}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:21+00","p50":3391.31653625,"p95":3680.0481444177885,"p99":3917.229885840495}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:22+00","p50":3496.960044125,"p95":3675.891394405253,"p99":3988.6810517554845}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:23+00","p50":3588.0013015,"p95":3970.010794856245,"p99":3993.1952528667202}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:24+00","p50":3507.8659012499998,"p95":4045.464292033862,"p99":4524.00201122088}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:25+00","p50":3206.570948125,"p95":4033.8412975238043,"p99":4039.2106606871803}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:26+00","p50":3305.9845111249997,"p95":3961.5744389665683,"p99":4587.6848417059155}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:27+00","p50":3175.489385,"p95":4003.7456571390735,"p99":4016.2049091827203}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:28+00","p50":3392.0925365000003,"p95":4186.586627129117,"p99":4195.956346854969}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:29+00","p50":3875.4065849999997,"p95":4144.301228146578,"p99":4163.9307424864155}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:30+00","p50":3284.5610034999995,"p95":4110.284232934055,"p99":4115.871576946662}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:31+00","p50":2879.8963512500004,"p95":4104.578853554038,"p99":4820.1665909921685}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:32+00","p50":3442.70165325,"p95":3983.821368585276,"p99":4766.606540647298}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:33+00","p50":2886.7767723750003,"p95":3838.742388839034,"p99":3855.6319229669552}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:34+00","p50":3490.9692187500004,"p95":4004.4817237604857,"p99":4028.8943793769913}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:35+00","p50":3739.093382625,"p95":3986.936870962782,"p99":3994.077658141179}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:36+00","p50":2402.703549125,"p95":4034.41542889592,"p99":4808.132513321634}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:37+00","p50":3040.2706978749998,"p95":4104.494041648662,"p99":4117.87050746793}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:38+00","p50":3262.1173417500004,"p95":4174.180728696279,"p99":4949.22516084986}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:39+00","p50":2934.98413625,"p95":4088.3273797855886,"p99":4860.6919417522395}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:40+00","p50":3282.8189617499997,"p95":4007.1072670844737,"p99":4595.612722016987}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:41+00","p50":2871.54755675,"p95":4128.582184930533,"p99":4268.961983897388}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:42+00","p50":2770.109360625,"p95":4292.6197854531365,"p99":4311.245175975731}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:43+00","p50":3607.490709,"p95":4216.042371309012,"p99":4265.44759972771}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:44+00","p50":3751.20592475,"p95":4279.674308426838,"p99":4965.854387371283}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:45+00","p50":4113.509405000001,"p95":4357.976472193981,"p99":4948.823955807859}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:46+00","p50":3667.85264725,"p95":4081.7744912903013,"p99":4117.657927946965}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:47+00","p50":3651.6874209999996,"p95":3987.252385312725,"p99":4016.283074921173}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:48+00","p50":2946.5649974999997,"p95":3840.213906821658,"p99":4379.3417496990805}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:49+00","p50":3838.1444045,"p95":3968.0099227007718,"p99":3977.9460275997326}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:50+00","p50":3767.370566,"p95":3870.719523430749,"p99":4510.923810435398}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:51+00","p50":2914.8397475,"p95":3960.9021677291435,"p99":3982.8998747030664}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:52+00","p50":3562.58605425,"p95":4028.074429193861,"p99":4763.402781900192}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:53+00","p50":3585.5748321250003,"p95":4087.7437080150166,"p99":4120.142563994101}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:54+00","p50":2450.5749885,"p95":4109.587137891972,"p99":4771.6716179763525}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:55+00","p50":3384.4732835,"p95":4126.503073297672,"p99":4176.230528521231}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:56+00","p50":3288.9615835000004,"p95":4180.379454150298,"p99":4981.01575641257}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:57+00","p50":4119.6292125,"p95":4321.295972078282,"p99":4341.307069215489}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:58+00","p50":3839.4022969999996,"p95":4277.114112797661,"p99":5152.068655907339}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:38:59+00","p50":3635.382338625,"p95":4313.660037833118,"p99":5066.888741769764}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:00+00","p50":2945.5677220000002,"p95":4258.168387695424,"p99":4983.27441748962}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:01+00","p50":2479.786123875,"p95":4056.5278506848254,"p99":4908.284345244088}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:02+00","p50":3071.540061625,"p95":4105.295595722141,"p99":4193.712735140906}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:03+00","p50":2576.989095625,"p95":4102.67090592998,"p99":4771.370187353686}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:04+00","p50":3297.6087775,"p95":4067.587001762827,"p99":4747.871007208458}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:05+00","p50":3035.4955090000003,"p95":4098.392093891248,"p99":4599.188799184939}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:06+00","p50":2679.8981595,"p95":3964.7464178558384,"p99":3999.5675737652045}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:07+00","p50":3492.4844297500003,"p95":3787.868725409244,"p99":3800.6355203912944}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:08+00","p50":3438.5118425,"p95":3967.2287080612964,"p99":4583.7640149268245}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:09+00","p50":3176.0642255,"p95":4156.482551570401,"p99":4173.919268782623}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:10+00","p50":3232.6827385,"p95":4185.551080640556,"p99":5009.437670666618}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:11+00","p50":2834.217328875,"p95":3987.9660789752406,"p99":4801.127123967161}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:12+00","p50":3125.098980625,"p95":3805.4465999459835,"p99":3819.7475286124886}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:13+00","p50":2915.3821865,"p95":3677.974073876093,"p99":4220.745344060722}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:14+00","p50":3248.0994945,"p95":3692.6479048658366,"p99":3718.525435324173}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:15+00","p50":3612.7746134999998,"p95":3701.8576559011995,"p99":4154.045306603012}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:16+00","p50":3216.4171845,"p95":3667.280217733736,"p99":4224.855497048531}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:17+00","p50":3228.647590875,"p95":3638.4640731389204,"p99":4038.6411179971306}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:18+00","p50":3363.0003065,"p95":3477.9344543242987,"p99":3495.7460667524006}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:19+00","p50":3150.1264182500004,"p95":3544.0654584551135,"p99":3553.3286184825324}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:20+00","p50":3294.867163875,"p95":3727.137435655499,"p99":3753.2816270056896}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:21+00","p50":3409.9209419999997,"p95":3953.6564826438394,"p99":4001.057150820722}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:22+00","p50":3778.08172725,"p95":3997.236203836522,"p99":4029.4127530442506}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:23+00","p50":3304.216138875,"p95":3910.0211517055222,"p99":3941.529629117682}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:24+00","p50":3213.2926196249996,"p95":4107.512915617416,"p99":4118.47568565994}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:25+00","p50":3487.737440875,"p95":4059.069457800716,"p99":4738.92641988887}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:26+00","p50":3568.410016375,"p95":4140.125964834496,"p99":4661.09604760116}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:27+00","p50":3101.0006926250003,"p95":4085.3329362167133,"p99":4818.794939449761}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:28+00","p50":3043.3802909999995,"p95":4224.091629992636,"p99":4892.283592519665}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:29+00","p50":3325.829699375,"p95":4271.396502848743,"p99":4302.074174345315}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:30+00","p50":3534.8073305000003,"p95":4234.497318408864,"p99":4255.482041352431}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:31+00","p50":2958.6323629999997,"p95":3957.0285432825367,"p99":4613.8224602489845}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:32+00","p50":3181.3262672499995,"p95":3831.9895231831874,"p99":4388.5889498181095}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:33+00","p50":3586.44449575,"p95":3687.882981591743,"p99":3715.569788311142}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:34+00","p50":3301.843729875,"p95":3754.065868436175,"p99":4328.927589434318}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:35+00","p50":3165.0888265,"p95":3638.382876938046,"p99":4157.968514408137}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:36+00","p50":3577.7404914999997,"p95":3679.746159709484,"p99":4106.552313309022}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:37+00","p50":3347.4094722500004,"p95":3636.528345124357,"p99":3656.916414292396}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:38+00","p50":3188.16195175,"p95":3485.6680883389317,"p99":3510.247739188611}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:39+00","p50":3292.345353375,"p95":3646.2210092753526,"p99":3700.3078788280445}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:40+00","p50":3235.9065251250004,"p95":3849.197352106697,"p99":3858.9897840825115}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:41+00","p50":3312.1398417499995,"p95":4106.344434254721,"p99":4142.680246986092}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:42+00","p50":3913.2528483749998,"p95":4101.8600837385875,"p99":4128.032046092005}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:43+00","p50":2903.7272645000003,"p95":3902.9201487427454,"p99":3926.341673231008}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:44+00","p50":3305.352916,"p95":3686.377829960074,"p99":3723.8546977233086}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:45+00","p50":3312.466879,"p95":3754.958865487721,"p99":4079.069299782924}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:46+00","p50":3259.192982875,"p95":3642.082794334479,"p99":3665.065476256521}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:47+00","p50":3488.207184125,"p95":3612.97594631652,"p99":3627.694272475731}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:48+00","p50":3449.82461525,"p95":3601.1392429987122,"p99":3849.75221489028}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:49+00","p50":3484.874687125,"p95":3547.671917648808,"p99":3623.982537797749}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:50+00","p50":3471.2718162499996,"p95":3533.956708577964,"p99":3742.8472208031008}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:51+00","p50":3440.42626525,"p95":3564.9332112385014,"p99":3792.666199861867}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:52+00","p50":3429.6472628750003,"p95":3494.162698170476,"p99":3719.4370087934653}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:53+00","p50":3437.7051864999994,"p95":3528.619020319118,"p99":3717.725611511246}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:54+00","p50":3455.0718975,"p95":3520.4096387469262,"p99":3688.6153952045934}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:55+00","p50":3388.349935875,"p95":3501.16131407243,"p99":3680.79208659506}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:56+00","p50":3486.3177095,"p95":3601.947107474822,"p99":3847.746394380711}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:57+00","p50":3504.7658,"p95":3776.726360879858,"p99":3794.01689193104}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:58+00","p50":3700.760456125,"p95":3993.627973420546,"p99":4399.076870315889}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:39:59+00","p50":3627.1649527500003,"p95":3994.170329721632,"p99":4368.677084438503}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:00+00","p50":3619.11160775,"p95":3928.437170380748,"p99":4258.325594286037}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:01+00","p50":3586.4758522499997,"p95":3812.329167381121,"p99":4119.719415258112}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:02+00","p50":3377.5379135,"p95":3777.9443169305296,"p99":3804.351017172784}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:03+00","p50":3407.918331,"p95":3581.669813550806,"p99":3590.2605072521587}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:04+00","p50":3333.139233125,"p95":3548.052602973781,"p99":3575.4627251781153}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:05+00","p50":3298.5731835,"p95":3443.3515321804457,"p99":3466.7032446276244}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:06+00","p50":3267.885459,"p95":3367.521168205141,"p99":3479.3558624675425}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:07+00","p50":3299.4347643749998,"p95":3448.7781796435447,"p99":3500.8704205701665}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:08+00","p50":3362.18360725,"p95":3511.2352924379984,"p99":3806.033098909977}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:09+00","p50":3418.5795636250004,"p95":3550.533564238281,"p99":3798.488485404526}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:10+00","p50":3387.5602129999997,"p95":3707.4978663804727,"p99":4029.490851103777}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:11+00","p50":3533.460601875,"p95":3705.9595355782544,"p99":3721.205219775323}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:12+00","p50":3357.740083875,"p95":3728.7532503274224,"p99":4036.861051842255}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:13+00","p50":3360.966099,"p95":3745.358903983276,"p99":3782.517723571459}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:14+00","p50":3481.3746815000004,"p95":3741.562530017086,"p99":3818.7240957239183}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:15+00","p50":3348.4714865,"p95":3810.2392059864333,"p99":3820.4487293047073}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:16+00","p50":3097.9350230000005,"p95":3809.969911378475,"p99":3827.659764729484}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:17+00","p50":3395.3582722499996,"p95":3914.8174906521776,"p99":4361.803687931677}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:18+00","p50":3629.657686,"p95":4019.769184716758,"p99":4037.855941411176}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:19+00","p50":3577.9349307499997,"p95":4077.073807990552,"p99":4682.113978095932}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:20+00","p50":3202.4249971249997,"p95":4096.561186033954,"p99":4117.028921476505}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:21+00","p50":3331.1648705,"p95":3923.2309090114486,"p99":3933.1614178745535}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:22+00","p50":3255.8094545,"p95":3881.5481343069023,"p99":4404.904657120085}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:23+00","p50":3623.4775,"p95":3879.9525457642744,"p99":3895.6488852341377}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:24+00","p50":3233.9252822500002,"p95":3872.3610017206965,"p99":3889.540258415253}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:25+00","p50":3408.7335405000003,"p95":3780.1966269609757,"p99":4293.425004396797}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:26+00","p50":3197.85104825,"p95":3936.6646702306402,"p99":4507.536116457112}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:27+00","p50":3637.70938725,"p95":3944.6333717200177,"p99":3984.8894609840095}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:28+00","p50":3358.539965,"p95":3906.6961773670378,"p99":4472.856948019926}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:29+00","p50":2937.468176,"p95":4092.4091973094482,"p99":4490.993397021701}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:30+00","p50":3139.32783,"p95":4056.356864952989,"p99":4503.262758234536}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:31+00","p50":3413.425915,"p95":3805.3808942153396,"p99":4470.584886380217}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:32+00","p50":2915.0338825,"p95":3804.6466601219536,"p99":4481.855271040502}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:33+00","p50":3452.021634,"p95":3917.2517836316015,"p99":3933.1859057793063}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:34+00","p50":2856.5065555,"p95":4102.721675457712,"p99":4638.941632043976}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:35+00","p50":2354.3730404999997,"p95":3985.1030412696073,"p99":4780.040067966511}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:36+00","p50":2965.6788941249997,"p95":3975.7407615057064,"p99":4692.696043212758}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:37+00","p50":2725.85233675,"p95":3929.035530191027,"p99":4750.880076310618}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:38+00","p50":2966.6716105,"p95":3927.6875365504557,"p99":4680.539851438203}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:39+00","p50":2838.362545875,"p95":4095.4811840818747,"p99":4784.998932164655}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:40+00","p50":3635.9811047499998,"p95":4141.8943428264765,"p99":4901.920456617757}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:41+00","p50":3003.526812,"p95":4059.329012350121,"p99":4075.004084308586}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:42+00","p50":3440.897803875,"p95":4065.66967426313,"p99":4914.484055824437}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:43+00","p50":3870.73400425,"p95":4101.202632000752,"p99":4209.879871893696}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:44+00","p50":3134.390871125,"p95":4219.050312715576,"p99":4916.519071203326}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:45+00","p50":3378.603587625,"p95":4289.943486261259,"p99":4304.7870919695615}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:46+00","p50":3096.36626575,"p95":4279.261573502634,"p99":4294.27801299727}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:47+00","p50":2874.276163,"p95":4089.4846679878074,"p99":4109.430297358721}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:48+00","p50":2823.1332295,"p95":4052.472774233339,"p99":4698.004947083006}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:49+00","p50":3406.04261825,"p95":4036.6771737689373,"p99":4113.048558642417}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:50+00","p50":3372.17788325,"p95":4000.079425564357,"p99":4542.761351106871}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:51+00","p50":3594.224319375,"p95":4115.450253114915,"p99":4260.670657157541}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:52+00","p50":3920.62865825,"p95":4158.464859343679,"p99":4708.954449021239}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:53+00","p50":3605.5772262500004,"p95":4086.624408758026,"p99":4636.565692624745}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:54+00","p50":3473.05211625,"p95":4099.812905108125,"p99":4166.437731680256}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:55+00","p50":3459.75113225,"p95":4026.75003785039,"p99":4415.356563003114}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:56+00","p50":3506.883413,"p95":4097.560454773061,"p99":4477.982716174077}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:57+00","p50":3297.645071,"p95":4051.520697720441,"p99":4116.411082245236}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:58+00","p50":3502.5658925,"p95":3991.570248906184,"p99":4453.003807052691}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:40:59+00","p50":3558.146333875,"p95":4092.811563391413,"p99":4357.57681506314}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:00+00","p50":3452.471091,"p95":4078.3676977832974,"p99":4102.995730995646}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:01+00","p50":3610.5584446250004,"p95":3886.8456458758283,"p99":3927.823954660213}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:02+00","p50":3659.85413475,"p95":3860.4956274579617,"p99":4263.164651326732}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:03+00","p50":3403.739342625,"p95":3902.989441246923,"p99":3917.7105270516386}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:04+00","p50":3313.4003107500002,"p95":3871.426579623915,"p99":4212.112849141096}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:05+00","p50":3179.80349575,"p95":3815.1666759108907,"p99":4247.501059921587}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:06+00","p50":3198.788998,"p95":3697.9791853988813,"p99":4208.429739588997}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:07+00","p50":2956.647693875,"p95":3751.146532461767,"p99":3769.101888975374}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:08+00","p50":3534.993801625,"p95":3811.027835953404,"p99":4347.633553049499}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:09+00","p50":3527.0326112499997,"p95":3812.446283359055,"p99":4271.650021182008}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:10+00","p50":3452.4561405000004,"p95":3749.3589962440137,"p99":3773.0855603650393}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:11+00","p50":3001.961251625,"p95":3687.1951611859195,"p99":4101.318755345249}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:12+00","p50":2852.287065,"p95":3764.489584760737,"p99":4258.316306576799}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:13+00","p50":3684.6984965,"p95":3791.85102621472,"p99":3801.7370944056997}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:14+00","p50":3405.1051595,"p95":3718.2314287388094,"p99":4296.036662458169}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:15+00","p50":2794.7393155,"p95":3626.4515137086146,"p99":4140.919348872962}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:16+00","p50":3329.66024025,"p95":3741.3870849158816,"p99":4287.798007183347}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:17+00","p50":3364.7238695,"p95":3678.815974987211,"p99":3691.5947403119294}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:18+00","p50":3400.581933875,"p95":3562.4110900672254,"p99":3845.786005642169}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:19+00","p50":3284.596498125,"p95":3690.8778787504057,"p99":3997.055299844102}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:20+00","p50":3461.99820475,"p95":3877.260218884794,"p99":4025.6441374849014}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:21+00","p50":3412.365991875,"p95":3966.421217451399,"p99":4356.032221628759}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:22+00","p50":3373.980134875,"p95":3686.570135681582,"p99":4154.967597959327}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:23+00","p50":3382.772892,"p95":3728.673875756445,"p99":4088.283643223026}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:24+00","p50":3583.1679157500002,"p95":3679.8847839356854,"p99":3701.1601374645406}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:25+00","p50":3471.14515975,"p95":3649.2081529219367,"p99":3865.431384734855}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:26+00","p50":3562.889736125,"p95":3738.0753121923203,"p99":3755.09460999646}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:27+00","p50":3581.0287685000003,"p95":3911.2420323764804,"p99":3973.726552929056}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:28+00","p50":3543.634772,"p95":3989.646731658327,"p99":4355.5202276441105}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:29+00","p50":3778.0228552500002,"p95":4099.629464545787,"p99":4118.777369081509}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:30+00","p50":3404.21920675,"p95":3993.4633986595295,"p99":4031.098784922464}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:31+00","p50":3459.7237595,"p95":3887.4685577993487,"p99":3914.3027238989976}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:32+00","p50":3154.039605,"p95":3809.9818387344844,"p99":3840.489269934718}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:33+00","p50":3698.68206525,"p95":4098.552125395957,"p99":4412.242336583504}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:34+00","p50":3025.381249,"p95":4016.8345396249892,"p99":4081.3857739675464}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:35+00","p50":3533.47238425,"p95":3924.4537812941558,"p99":4533.358512987344}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:36+00","p50":3089.69832525,"p95":3695.217831758813,"p99":3723.905614325424}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:37+00","p50":3030.02693575,"p95":3655.321474210323,"p99":4132.4593656271945}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:38+00","p50":3494.828334375,"p95":3799.378373246664,"p99":3810.974890998402}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:39+00","p50":3105.4319167500003,"p95":3958.5575952206127,"p99":4442.581104371212}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:40+00","p50":3915.3236875000002,"p95":4014.505003878078,"p99":4465.476259516889}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:41+00","p50":3121.6836055,"p95":4033.633329904034,"p99":4052.9609215629434}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:42+00","p50":3212.925734,"p95":3810.8793724015563,"p99":3921.585736007909}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:43+00","p50":3259.5963973750004,"p95":3929.99866704877,"p99":4459.455710302825}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:44+00","p50":3380.54413425,"p95":3854.0216954483985,"p99":4283.55217776917}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:45+00","p50":3506.6499327499996,"p95":3799.0615468948417,"p99":4169.9160559460715}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:46+00","p50":3348.7515495,"p95":3700.647661533427,"p99":3969.670152839774}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:47+00","p50":3285.1935245,"p95":3519.124160684575,"p99":3704.5022614698523}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:48+00","p50":3308.9245579999997,"p95":3425.4175101093883,"p99":3440.7119554302967}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:49+00","p50":3429.617882875,"p95":3541.1142726158864,"p99":3620.042182079638}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:50+00","p50":3485.45680525,"p95":3641.178695851225,"p99":3758.8729772077772}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:51+00","p50":3526.9651235,"p95":3700.507463949268,"p99":3721.2613390284114}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:52+00","p50":3579.3737303750004,"p95":3751.361215318446,"p99":4088.80130344427}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:53+00","p50":3350.65459125,"p95":3725.8870364963277,"p99":4005.44801856883}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:54+00","p50":3643.3746575,"p95":3812.3617805807726,"p99":3828.469597416357}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:55+00","p50":3436.474518125,"p95":3877.452325059154,"p99":4250.994766467559}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:56+00","p50":3497.2605355,"p95":3706.0270635985034,"p99":4103.980096321481}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:57+00","p50":3546.3447134999997,"p95":3675.1107862342465,"p99":4006.2843002244713}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:58+00","p50":3542.23079825,"p95":3662.0283556928916,"p99":3691.9436009443098}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:41:59+00","p50":3417.3317913749997,"p95":3664.2082282891433,"p99":3673.1911535439626}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:00+00","p50":3323.036161375,"p95":3402.1379723167242,"p99":3600.6542268613734}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:01+00","p50":3241.7306223749997,"p95":3405.656879168784,"p99":3424.5227655033013}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:02+00","p50":3202.6613770000004,"p95":3517.7114740942516,"p99":3538.4088282182615}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:03+00","p50":3351.764129,"p95":3514.0506483085383,"p99":3769.645283644115}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:04+00","p50":3333.30744375,"p95":3514.5829778595794,"p99":3611.0409226095567}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:05+00","p50":3313.3261709999997,"p95":3431.663140975411,"p99":3455.5892938771767}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:06+00","p50":3148.549544875,"p95":3421.60272021968,"p99":3437.3848267064395}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:07+00","p50":3240.046666,"p95":3460.1014381690147,"p99":3477.1425023731285}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:08+00","p50":3330.314868,"p95":3564.2639444260526,"p99":3891.138504150167}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:09+00","p50":3337.0925105,"p95":3556.802810899663,"p99":3741.926497246622}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:10+00","p50":3390.1861049999998,"p95":3587.7882809657976,"p99":3628.324214963173}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:11+00","p50":3308.6252125,"p95":3398.775116703263,"p99":3563.1196082221454}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:12+00","p50":3141.7865702500003,"p95":3412.6411501920693,"p99":3450.823118857816}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:13+00","p50":3218.933468,"p95":3567.8708596183574,"p99":3857.446652206736}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:14+00","p50":3213.33055475,"p95":3628.8853813374853,"p99":4012.8986411593874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:15+00","p50":3183.0811866250006,"p95":3606.748389013644,"p99":3646.1110445022146}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:16+00","p50":3221.7830147500003,"p95":3523.953968831139,"p99":3545.2718205986416}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:17+00","p50":3199.884182,"p95":3474.43311849904,"p99":3493.0611986963827}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:18+00","p50":3398.37037925,"p95":3603.062362684314,"p99":3620.23825351204}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:19+00","p50":3442.9705162500004,"p95":3805.042028834554,"p99":3819.024470849407}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:20+00","p50":3734.6542019999997,"p95":3882.653097465944,"p99":3922.119958332046}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:21+00","p50":3365.042242,"p95":3942.141542313028,"p99":4422.726813588053}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:22+00","p50":3856.023993125,"p95":4027.70140514744,"p99":4653.834375491782}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:23+00","p50":3418.03793475,"p95":3943.993509512265,"p99":3964.0168385461566}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:24+00","p50":3589.208109,"p95":3945.399548000845,"p99":4496.719866312634}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:25+00","p50":3755.194479875,"p95":3987.525313856741,"p99":4412.088862214668}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:26+00","p50":3344.53799025,"p95":3988.5660170404167,"p99":4029.571523633874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:27+00","p50":3796.3672975,"p95":3895.6115038215735,"p99":3973.9887536478386}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:28+00","p50":3761.9019856249997,"p95":3883.284016048738,"p99":3973.779319571269}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:29+00","p50":3735.581153,"p95":3849.3372113382225,"p99":3874.3156529999083}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:30+00","p50":3669.6999790000004,"p95":3800.801851428093,"p99":4058.1330713904163}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:31+00","p50":3507.349128625,"p95":3706.917465341931,"p99":4003.808990964062}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:32+00","p50":3440.8277871249998,"p95":3521.9945045333434,"p99":3795.5223364661856}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:33+00","p50":3478.0638452499998,"p95":3599.027089170467,"p99":3627.071014257535}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:34+00","p50":3428.569566,"p95":3509.048791762789,"p99":3683.526707632329}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:35+00","p50":3382.853085,"p95":3451.173850838957,"p99":3721.53300079377}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:36+00","p50":3307.7698681250004,"p95":3440.0256989659933,"p99":3633.7041731985432}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:37+00","p50":3211.8251060000002,"p95":3259.4251887184105,"p99":3376.228246290939}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:38+00","p50":3316.806936125,"p95":3489.961528636672,"p99":3505.2643747067386}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:39+00","p50":3426.1740772499998,"p95":3511.4041188258534,"p99":3615.0670138808405}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:40+00","p50":3423.9295890000003,"p95":3510.23464298403,"p99":3526.660272595446}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:41+00","p50":3430.4840802500003,"p95":3630.3084360498024,"p99":3884.6792710087634}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:42+00","p50":3381.5377900000003,"p95":3434.769765475745,"p99":3645.409401722426}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:43+00","p50":3370.381812,"p95":3439.998185296606,"p99":3627.612137580475}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:44+00","p50":3364.802525125,"p95":3446.7608034941713,"p99":3598.148154461615}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:45+00","p50":3421.8180315,"p95":3500.244751774954,"p99":3514.8409244762875}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:46+00","p50":3291.837262125,"p95":3516.8955349423804,"p99":3802.032157146258}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:47+00","p50":3278.219354625,"p95":3350.2368528069933,"p99":3540.3332910533477}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:48+00","p50":3322.561548875,"p95":3384.896384525378,"p99":3588.269347293802}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:49+00","p50":3286.4612335,"p95":3397.5848920492717,"p99":3406.627369794732}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:50+00","p50":3285.0147201249997,"p95":3469.832910078948,"p99":3504.138387789175}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:51+00","p50":3302.8229097500007,"p95":3525.2475858052258,"p99":3546.311170353913}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:52+00","p50":3417.545737,"p95":3559.179386570719,"p99":3574.6789338455887}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:53+00","p50":3453.2242673749997,"p95":3541.038026322145,"p99":3552.5925635993526}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:54+00","p50":3431.5601688750003,"p95":3490.0335644840206,"p99":3524.108693756973}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:55+00","p50":3473.189215125,"p95":3550.52352991464,"p99":3770.6458714309574}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:56+00","p50":3462.1719942500004,"p95":3605.938515912914,"p99":3673.730403148387}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:57+00","p50":3580.64694775,"p95":3834.6341673465004,"p99":3861.4096818676667}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:58+00","p50":3683.94309725,"p95":3840.307878111153,"p99":3847.335714567927}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:42:59+00","p50":3557.9044035,"p95":3842.138335570509,"p99":4049.5032011173325}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:00+00","p50":3480.8115435,"p95":3640.0227432973443,"p99":3814.888986858942}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:01+00","p50":3409.6012647499997,"p95":3662.777206113112,"p99":3831.58926605165}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:02+00","p50":3373.5670355,"p95":3431.750156154127,"p99":3559.328521564598}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:03+00","p50":3391.6554615,"p95":3585.4712988756683,"p99":3604.8782150558327}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:04+00","p50":3439.3741865,"p95":3511.9320253302067,"p99":3524.352783927312}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:05+00","p50":3438.197827125,"p95":3524.4722749882444,"p99":3753.6978435186434}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:06+00","p50":3363.6905914999998,"p95":3557.201978048016,"p99":3738.717403231031}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:07+00","p50":3273.616982375,"p95":3437.8814984564406,"p99":3571.5434379035883}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:08+00","p50":3307.7235467499995,"p95":3362.8175278751,"p99":3632.307369446304}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:09+00","p50":3334.69099375,"p95":3453.718284433818,"p99":3602.513112540376}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:10+00","p50":3416.0336265,"p95":3598.915246998626,"p99":3615.438639143141}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:11+00","p50":3435.941654625,"p95":3516.183491420521,"p99":3768.3974083054463}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:12+00","p50":3345.0191729999997,"p95":3508.1330786689236,"p99":3518.3939052718565}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:13+00","p50":3232.960544625,"p95":3421.386327141643,"p99":3436.893748143761}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:14+00","p50":3295.106593,"p95":3420.4845115283074,"p99":3749.075588466954}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:15+00","p50":3165.7956544999997,"p95":3474.7694172150955,"p99":3500.043711489738}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:16+00","p50":3330.965524,"p95":3428.107583749073,"p99":3743.2087153125603}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:17+00","p50":3247.0498365000003,"p95":3429.7679371769486,"p99":3699.776482936511}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:18+00","p50":3273.5456022500002,"p95":3481.0601914903064,"p99":3788.6436799291246}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:19+00","p50":3322.383813,"p95":3514.76177673705,"p99":3840.8471132756}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:20+00","p50":3313.1562860000004,"p95":3526.173407474133,"p99":3545.274675311586}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:21+00","p50":3242.488652,"p95":3350.6272994486735,"p99":3506.9337341229325}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:22+00","p50":3202.07466375,"p95":3300.6235212799397,"p99":3489.922360644884}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:23+00","p50":3249.1976265,"p95":3318.409008260099,"p99":3577.2182582290484}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:24+00","p50":3311.6607912500003,"p95":3382.5993578735533,"p99":3473.8884911120344}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:25+00","p50":3295.6972625,"p95":3472.8681825518975,"p99":3567.368194764436}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:26+00","p50":3317.22166275,"p95":3505.3677016668416,"p99":3515.040056990616}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:27+00","p50":3376.03641875,"p95":3667.712251967427,"p99":3832.7632094474657}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:28+00","p50":3561.9383655,"p95":3780.1635977092133,"p99":3800.8446053921916}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:29+00","p50":3368.5154577499998,"p95":3819.4853629312342,"p99":4209.5862012740035}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:30+00","p50":3276.5937725000003,"p95":3839.9585402304742,"p99":3862.084033411764}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:31+00","p50":3046.8698409999997,"p95":3715.430269544366,"p99":3755.163319732658}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:32+00","p50":3634.5424646250003,"p95":3804.3449961135175,"p99":3815.9031024700844}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:33+00","p50":3325.69433175,"p95":3804.6009638691125,"p99":3819.9997822946207}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:34+00","p50":3106.2216936249997,"p95":3804.841503845843,"p99":3838.1584369881443}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:35+00","p50":3433.68727175,"p95":3684.2733120840167,"p99":4187.020498129905}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:36+00","p50":3582.486955,"p95":3688.0734682963753,"p99":4153.844523449737}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:37+00","p50":3057.253497625,"p95":3617.32249078229,"p99":4043.1702092083483}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:38+00","p50":2972.737958,"p95":3673.4316852876536,"p99":4195.4222453747525}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:39+00","p50":2966.83881025,"p95":3545.4276131259,"p99":3866.8736200156313}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:40+00","p50":3064.909732125,"p95":3606.6714737699904,"p99":3621.4747569819247}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:41+00","p50":3211.03422525,"p95":3537.334442243527,"p99":3598.842983187475}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:42+00","p50":3258.199005375,"p95":3463.738836587829,"p99":3482.5003042471694}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:43+00","p50":3388.4871872500003,"p95":3580.911515058687,"p99":3609.70700802333}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:44+00","p50":3457.949436125,"p95":3587.9095786095445,"p99":3602.9300184844815}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:45+00","p50":3555.9663105000004,"p95":3609.350586966885,"p99":3838.9705409364437}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:46+00","p50":3562.588873375,"p95":3678.6121187963936,"p99":3870.782228963701}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:47+00","p50":3484.188703375,"p95":3606.0081861049766,"p99":3618.609891334936}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:48+00","p50":3519.0376484999997,"p95":3667.5998839104727,"p99":3701.46962719833}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:49+00","p50":3564.7411015,"p95":3698.0836422391235,"p99":3719.4112565151813}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:50+00","p50":3473.051057625,"p95":3819.6127420083308,"p99":3837.1019482648635}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:51+00","p50":3427.15331325,"p95":3800.176373766976,"p99":3842.8704879508055}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:52+00","p50":3613.188064125,"p95":3766.446186750344,"p99":4058.4399844496374}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:53+00","p50":3628.895954875,"p95":3785.2589792592053,"p99":4080.2648658848816}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:54+00","p50":3547.462674,"p95":3849.0666991192716,"p99":3870.7773556974344}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:55+00","p50":3432.0735026250004,"p95":3720.9180916747287,"p99":4100.903120687401}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:56+00","p50":3415.617523875,"p95":3488.8351553589587,"p99":3791.334548814896}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:57+00","p50":3515.39850575,"p95":3667.8983047507318,"p99":3698.268309774481}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:58+00","p50":3707.1106687499996,"p95":3800.7394348058083,"p99":3840.2944584047577}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:43:59+00","p50":3745.16737225,"p95":3997.6139140298287,"p99":4015.0406471599495}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:00+00","p50":3671.4960659999997,"p95":3946.9215303115157,"p99":3986.943571203165}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:01+00","p50":3495.290978,"p95":3562.8269061476267,"p99":3798.522177353886}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:02+00","p50":3439.1047635000004,"p95":3495.6275313755796,"p99":3579.9545746844387}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:03+00","p50":3314.519146,"p95":3419.143279464043,"p99":3541.818598490643}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:04+00","p50":3249.912410125,"p95":3473.27905596262,"p99":3489.7289356206447}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:05+00","p50":3202.644486,"p95":3452.0255869158595,"p99":3467.6811181346206}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:06+00","p50":3168.741986,"p95":3466.6219744894415,"p99":3486.610334222781}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:07+00","p50":3187.20684325,"p95":3503.5919109459664,"p99":3528.069459552975}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:08+00","p50":3345.0875505000004,"p95":3643.6266880021362,"p99":4043.4308445403026}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:09+00","p50":3481.0953745,"p95":3685.3994592746417,"p99":3713.19845572846}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:10+00","p50":3240.643855875,"p95":3704.5230539001436,"p99":3714.8558591666483}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:11+00","p50":3314.89459075,"p95":3709.0687735853508,"p99":3723.3067748413873}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:12+00","p50":3428.405594,"p95":3495.9126604674816,"p99":3855.9198338114047}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:13+00","p50":3291.371267,"p95":3498.373650075741,"p99":3743.103622177964}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:14+00","p50":3292.3737515000003,"p95":3395.9727380702248,"p99":3446.60296605961}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:15+00","p50":3316.7429825,"p95":3515.1530350639923,"p99":3540.767967496416}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:16+00","p50":3392.0038055000005,"p95":3515.0858508782694,"p99":3536.4045091518756}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:17+00","p50":3093.7310375,"p95":3556.696561597489,"p99":3583.798430713024}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:18+00","p50":3158.8241920000005,"p95":3653.8410186280244,"p99":4035.6423950602066}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:19+00","p50":3482.776076,"p95":3731.669162407945,"p99":3743.562263213534}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:20+00","p50":3265.37714625,"p95":3618.760152324948,"p99":3634.277825679383}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:21+00","p50":3380.23357425,"p95":3601.163824076834,"p99":3609.415641502574}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:22+00","p50":3281.819768625,"p95":3566.612456957562,"p99":3999.0193059358767}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:23+00","p50":3227.5561900000002,"p95":3727.3483069306767,"p99":4013.701882028955}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:24+00","p50":3341.09901425,"p95":3764.653255019227,"p99":4213.707698050909}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:25+00","p50":3554.5386337500004,"p95":3737.236671800909,"p99":4194.673240070561}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:26+00","p50":3340.7448560000003,"p95":3723.1990760304993,"p99":4209.279019433308}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:27+00","p50":3174.5821875,"p95":3920.2290307000726,"p99":3941.0706167855933}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:28+00","p50":3341.7416848749995,"p95":3956.260530949452,"p99":4387.1272296427915}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:29+00","p50":3489.96157525,"p95":4040.020814656356,"p99":4076.0953603980984}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:30+00","p50":3283.11828525,"p95":4004.91574344699,"p99":4024.11718714808}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:31+00","p50":2993.3656737499996,"p95":4070.3572242700216,"p99":4536.135481352859}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:32+00","p50":3728.9784205,"p95":4139.2384580796515,"p99":4148.521261387966}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:33+00","p50":3070.88208475,"p95":4061.7445449225115,"p99":4100.571406096752}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:34+00","p50":3143.9882215,"p95":3924.4456037242885,"p99":4714.293944452117}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:35+00","p50":3372.5991372500002,"p95":3750.62781243275,"p99":4343.0365524011}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:36+00","p50":3310.777132,"p95":3521.1916326086825,"p99":3738.7619867710787}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:37+00","p50":3394.31246475,"p95":3581.3409649760765,"p99":3798.4597312917194}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:38+00","p50":3417.806976,"p95":3494.7929174889678,"p99":3547.261527936111}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:39+00","p50":3446.1641119999995,"p95":3502.5912654008143,"p99":3574.262673119554}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:40+00","p50":3380.7104525,"p95":3469.249601956289,"p99":3487.464125256155}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:41+00","p50":3507.9467357500002,"p95":3642.062441988497,"p99":3655.7853526532954}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:42+00","p50":3390.340435625,"p95":3538.416038249907,"p99":3597.026718731573}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:43+00","p50":3431.97218675,"p95":3542.076341819704,"p99":3567.0893513761744}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:44+00","p50":3412.759875375,"p95":3618.6514721150356,"p99":3993.925092869799}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:45+00","p50":3381.2643695,"p95":3599.8234880511313,"p99":3661.464387048414}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:46+00","p50":3298.3819170000006,"p95":3506.1093518634752,"p99":3515.690151120074}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:47+00","p50":3256.911897,"p95":3528.2320852309017,"p99":3905.8695854605485}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:48+00","p50":3048.466126375,"p95":3682.350990099143,"p99":3726.170242531051}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:49+00","p50":3054.18692675,"p95":3688.6811873772285,"p99":3734.9072249657806}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:50+00","p50":3114.5809314999997,"p95":3771.014370428611,"p99":3812.2713212381404}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:51+00","p50":2908.8382728750003,"p95":3917.7121875678604,"p99":3997.3788204324183}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:52+00","p50":3009.6962970000004,"p95":4014.490339367228,"p99":4043.0878818845754}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:53+00","p50":2845.9318615,"p95":4045.253691178872,"p99":4721.466733598959}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:54+00","p50":3467.62101475,"p95":4059.553599111917,"p99":4746.3600984698605}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:55+00","p50":3678.68427375,"p95":3878.6645704162465,"p99":4419.771222428656}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:56+00","p50":2853.640628,"p95":3781.9845796724308,"p99":3845.717120369373}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:57+00","p50":3081.55624,"p95":4062.971499631526,"p99":4130.849381938227}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:58+00","p50":2984.48075375,"p95":4204.197447813499,"p99":4759.502322837851}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:44:59+00","p50":3534.4104355,"p95":4170.452379977139,"p99":4195.314634537895}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:00+00","p50":3008.6791865,"p95":4004.032316235226,"p99":4695.121121447077}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:01+00","p50":3156.052586375,"p95":3670.1962001837906,"p99":4079.6229477651805}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:02+00","p50":3274.118938,"p95":3804.7568758644784,"p99":4254.948103784571}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:03+00","p50":3407.380955875,"p95":3743.2522877895503,"p99":3756.35679284567}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:04+00","p50":3210.4213855,"p95":3786.205901142225,"p99":4287.958629436217}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:05+00","p50":3392.9208710000003,"p95":3742.714312367185,"p99":4178.264236308874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:06+00","p50":3030.640124,"p95":3794.874264264259,"p99":3805.4034485755537}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:07+00","p50":3140.491677375,"p95":3855.5765395846606,"p99":3896.010526315867}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:08+00","p50":3526.874414,"p95":3863.3287231119784,"p99":4484.856240266006}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:09+00","p50":2924.190724125,"p95":3923.0884249842097,"p99":3936.943363748955}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:10+00","p50":3577.386766625,"p95":3884.4397670860353,"p99":4430.003999646722}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:11+00","p50":3360.3012555,"p95":3945.7009465875362,"p99":3999.4069915497284}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:12+00","p50":3235.31693,"p95":4034.0341450959395,"p99":4070.063423194797}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:13+00","p50":2824.80914175,"p95":3950.683273148908,"p99":4612.491283640465}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:14+00","p50":3570.178240375,"p95":4005.007945192515,"p99":4711.826173349874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:15+00","p50":3256.2824032500002,"p95":4114.949805581018,"p99":4844.725650744604}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:16+00","p50":2452.7261325,"p95":4146.880633463845,"p99":4781.031431302063}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:17+00","p50":3837.772018875,"p95":3933.3174533478,"p99":4828.217716855897}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:18+00","p50":3576.32374825,"p95":3854.1395838460094,"p99":4534.083438846875}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:19+00","p50":3484.5322095,"p95":3831.5759877478417,"p99":4369.78275328379}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:20+00","p50":3691.7698067499996,"p95":3934.0069822735254,"p99":4526.889991671799}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:21+00","p50":3610.3397021250003,"p95":4181.020279403292,"p99":4762.849447504053}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:22+00","p50":3395.0510917499996,"p95":4187.007132177324,"p99":4195.47592349741}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:23+00","p50":3512.84374325,"p95":4303.222359592383,"p99":4764.484265889057}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:24+00","p50":3568.1770145,"p95":4324.4333452773635,"p99":4348.285751520143}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:25+00","p50":3224.8545411249997,"p95":4074.93459055334,"p99":4611.595441349553}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:26+00","p50":3262.24451875,"p95":4052.184372837228,"p99":4092.853540683972}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:27+00","p50":3555.0186145,"p95":4162.434932285495,"p99":4218.716174018143}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:28+00","p50":3547.677024875,"p95":4125.750039671256,"p99":4798.165878476158}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:29+00","p50":3989.4513214999997,"p95":4135.457350714762,"p99":4745.245185843205}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:30+00","p50":3076.076053875,"p95":4072.1460142588708,"p99":4695.193476648898}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:31+00","p50":3825.435297,"p95":3981.7064972772814,"p99":4006.461149607312}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:32+00","p50":3510.203456,"p95":3897.4635251164764,"p99":3912.4305661164485}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:33+00","p50":3064.7352015,"p95":3827.041297553948,"p99":4375.547026624866}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:34+00","p50":3093.70877,"p95":3785.3801177828186,"p99":4251.916568350838}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:35+00","p50":3441.1845089999997,"p95":3752.1982981172446,"p99":4326.58852013782}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:36+00","p50":3142.9271926250003,"p95":3808.6405156599935,"p99":4395.368254505509}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:37+00","p50":3058.509834875,"p95":3912.659409333504,"p99":3940.9438540950528}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:38+00","p50":2888.176477125,"p95":3952.5723889278797,"p99":4592.662579527643}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:39+00","p50":2653.0458412499997,"p95":4058.6225503412516,"p99":4483.097354083012}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:40+00","p50":3254.4653504999997,"p95":4097.799384347052,"p99":4121.691369113869}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:41+00","p50":3262.0244965,"p95":4121.888491481417,"p99":4811.165995634854}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:42+00","p50":3514.657188,"p95":4092.120416701202,"p99":4850.99170350807}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:43+00","p50":3453.10251925,"p95":3976.724310362448,"p99":3997.6589110460186}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:44+00","p50":2860.0387702499997,"p95":4015.2331305826015,"p99":4677.780902354109}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:45+00","p50":3167.033755,"p95":4055.665263498683,"p99":4811.829952179042}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:46+00","p50":2954.865083625,"p95":3944.902688811742,"p99":4635.495074453713}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:47+00","p50":3511.3304693749997,"p95":3944.9917604694388,"p99":3952.3233389205607}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:48+00","p50":2953.6829445000003,"p95":3944.667445614446,"p99":3987.175388447919}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:49+00","p50":3184.3556115,"p95":3995.6711308892245,"p99":4588.349824622306}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:50+00","p50":3245.232853,"p95":4047.0355042768156,"p99":4078.7247063337736}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:51+00","p50":3272.7015064999996,"p95":4102.94292038606,"p99":4145.135979863276}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:52+00","p50":3302.775589,"p95":3985.7371457737713,"p99":4034.6504223441075}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:53+00","p50":3356.0470640000003,"p95":3850.63757135436,"p99":4497.027984969142}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:54+00","p50":3133.174666,"p95":3916.068990583954,"p99":3957.418218480313}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:55+00","p50":3311.637166,"p95":4016.0444647100267,"p99":4045.40963406667}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:56+00","p50":3135.376848,"p95":4329.718253544681,"p99":4507.872302845333}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:57+00","p50":4014.1519570000005,"p95":4323.172308580673,"p99":4373.670450996421}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:58+00","p50":3571.207194,"p95":4353.039684107162,"p99":4455.910671431186}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:45:59+00","p50":3919.1732002500003,"p95":4219.57739209673,"p99":4561.8693803433725}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:00+00","p50":3551.673055875,"p95":3964.6780622020556,"p99":4344.732773514781}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:01+00","p50":3562.444708,"p95":3809.614094356959,"p99":3966.549394656354}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:02+00","p50":3265.06037975,"p95":3889.445747927346,"p99":4308.854899800805}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:03+00","p50":3026.85220725,"p95":3913.957838936094,"p99":3953.6649786396174}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:04+00","p50":3513.6412362499996,"p95":3881.4052651170505,"p99":4316.573581009049}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:05+00","p50":3015.2245097500004,"p95":3885.343318361877,"p99":4320.64179338517}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:06+00","p50":3282.0026820000003,"p95":3873.7942803314872,"p99":4354.437183535307}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:07+00","p50":3003.858284,"p95":3898.1164975124193,"p99":3925.74472009803}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:08+00","p50":3496.53033775,"p95":3960.190122896726,"p99":4581.9397418089975}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:09+00","p50":3604.655357875,"p95":4126.852990694547,"p99":4648.414359381075}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:10+00","p50":3058.54159,"p95":4144.754648382955,"p99":4160.459870245163}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:11+00","p50":3250.796862625,"p95":3947.283872782859,"p99":4536.481641486386}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:12+00","p50":3589.5050892500003,"p95":3945.8407672898434,"p99":4467.595299372991}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:13+00","p50":2863.9111345,"p95":3897.2364883101295,"p99":3969.2418056006395}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:14+00","p50":3290.693428875,"p95":3749.98816379957,"p99":4236.763037094788}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:15+00","p50":2667.766225,"p95":3847.6907836420573,"p99":3864.1868519504055}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:16+00","p50":2939.4164545,"p95":3876.829897323595,"p99":3923.781161713507}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:17+00","p50":3378.9195798749997,"p95":3958.7490394492183,"p99":4639.728457001336}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:18+00","p50":2858.895520125,"p95":4057.769638528722,"p99":4637.63195149092}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:19+00","p50":3213.126814,"p95":3982.6377243681213,"p99":4660.941550276893}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:20+00","p50":2776.72606575,"p95":3965.2789414669232,"p99":4637.841828644301}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:21+00","p50":3295.41928925,"p95":3906.2836602676225,"p99":3928.55446655739}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:22+00","p50":3717.3166461250003,"p95":3882.101225941232,"p99":3894.1257586438614}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:23+00","p50":3717.572256875,"p95":3787.4121563758504,"p99":4376.780862479451}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:24+00","p50":3229.731394375,"p95":3918.6113518809866,"p99":4376.706883999279}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:25+00","p50":3393.798085,"p95":3892.9390089590124,"p99":3908.11845126317}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:26+00","p50":3742.41139025,"p95":3927.7719392848207,"p99":4398.307551285478}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:27+00","p50":3632.640388125,"p95":3994.0153595869997,"p99":4017.1729880425637}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:28+00","p50":3756.2000735,"p95":3860.68769260561,"p99":4396.701493619236}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:29+00","p50":3456.3044835,"p95":3843.1437012613137,"p99":4204.665953552855}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:30+00","p50":3309.928166,"p95":3937.3981132091594,"p99":3943.0400085689294}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:31+00","p50":3330.99900975,"p95":3876.7873832358537,"p99":4409.6061712189885}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:32+00","p50":2989.7619535000003,"p95":3636.8261871825584,"p99":3664.8717693570206}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:33+00","p50":3046.1499962499997,"p95":3725.4724066044932,"p99":3745.466084611152}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:34+00","p50":3424.1305985,"p95":3598.72058023381,"p99":4079.043965190735}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:35+00","p50":3012.260397,"p95":3624.60142299885,"p99":3648.555793490215}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:36+00","p50":2928.7075817500004,"p95":3663.7615418037035,"p99":4222.831573580309}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:37+00","p50":3161.679953,"p95":3702.5126238743287,"p99":4243.1968400568285}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:38+00","p50":3456.226651,"p95":3711.293741967384,"p99":3759.6058081494066}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:39+00","p50":3125.741794625,"p95":3861.488542721633,"p99":4257.015018921065}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:40+00","p50":3453.19400575,"p95":3941.539737328736,"p99":4389.787553564991}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:41+00","p50":3084.5116245,"p95":3939.0162979483835,"p99":4450.84407599875}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:42+00","p50":2991.7287045,"p95":4096.693002093605,"p99":4180.278913550498}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:43+00","p50":3547.385372,"p95":4255.771056502971,"p99":4272.463285918034}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:44+00","p50":3545.91673,"p95":4195.472760818875,"p99":4281.906329002233}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:45+00","p50":4264.8083879999995,"p95":4404.946333520694,"p99":4553.604765739531}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:46+00","p50":3381.19285475,"p95":4458.304842994802,"p99":4483.931625893602}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:47+00","p50":3600.471814,"p95":4152.482586959613,"p99":4765.274225137505}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:48+00","p50":3224.04529225,"p95":4044.0383215760066,"p99":4572.341392854828}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:49+00","p50":3375.1717685000003,"p95":4024.879541049447,"p99":4085.9601104393196}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:50+00","p50":3632.8975582499997,"p95":3905.7141105130895,"p99":3951.131275141826}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:51+00","p50":3485.553513125,"p95":4010.9457817608595,"p99":4027.350401384947}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:52+00","p50":3930.8261955,"p95":4119.9990047458,"p99":4607.477795255713}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:53+00","p50":3555.7089495,"p95":4061.87458469013,"p99":4604.818032177989}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:54+00","p50":3572.9101960000003,"p95":3997.2457938601397,"p99":4031.7284173945422}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:55+00","p50":3537.6818055000003,"p95":3831.050422709502,"p99":4092.128705460907}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:56+00","p50":3690.825774,"p95":3845.8599736749925,"p99":3928.8958601310064}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:57+00","p50":3747.971465,"p95":3824.2702367946454,"p99":4198.5702952486035}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:58+00","p50":3844.360855875,"p95":3960.0404004706916,"p99":4075.6885944592113}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:46:59+00","p50":3957.15127225,"p95":4034.5368271656816,"p99":4259.229125750352}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:00+00","p50":3839.8925010000003,"p95":4019.8446821899684,"p99":4047.3618029119484}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:01+00","p50":3675.81592475,"p95":3821.5978975397134,"p99":3966.178968359014}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:02+00","p50":3541.704081,"p95":3618.787492765593,"p99":3691.0505888052403}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:03+00","p50":3554.885815,"p95":3687.4943648173567,"p99":4007.5952738166297}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:04+00","p50":3375.3036153750004,"p95":3714.221693369584,"p99":3733.8872826356246}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:05+00","p50":3474.417569625,"p95":3688.5521474584384,"p99":3702.032238212965}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:06+00","p50":3386.4985392500002,"p95":3829.624933923929,"p99":4314.698300925144}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:07+00","p50":3739.725271,"p95":3833.5873772035675,"p99":3867.946394027468}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:08+00","p50":3092.0967689999998,"p95":3904.8993871643233,"p99":3918.0418222000844}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:09+00","p50":3216.0638987499997,"p95":3903.5775088169166,"p99":3926.225784326961}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:10+00","p50":3500.4858927500004,"p95":3913.203282331455,"p99":4512.0346235982615}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:11+00","p50":3536.537817125,"p95":3858.9353433982683,"p99":3869.541006168625}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:12+00","p50":3296.4130480000003,"p95":3807.50679445697,"p99":3828.5432715896222}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:13+00","p50":3236.3970369999997,"p95":3665.8084024485825,"p99":3688.6618830680845}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:14+00","p50":3481.8169347499997,"p95":3757.3315476099215,"p99":3772.7036508238316}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:15+00","p50":3346.2902704999997,"p95":3854.048202262288,"p99":3993.3642988881547}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:16+00","p50":3564.79711675,"p95":3888.1781375634937,"p99":4479.785111776903}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:17+00","p50":3089.571034875,"p95":3829.9945088519007,"p99":4425.107977444142}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:18+00","p50":3175.707762,"p95":3890.893825990255,"p99":4442.191116398439}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:19+00","p50":3679.1207245,"p95":3773.204703409842,"p99":3789.525295880859}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:20+00","p50":3185.95247875,"p95":3813.6397482133207,"p99":3825.698553847472}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:21+00","p50":3392.5543609999995,"p95":3750.7997763205517,"p99":4358.070276889039}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:22+00","p50":3213.461616875,"p95":3795.731509748107,"p99":4218.2456386374415}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:23+00","p50":3626.832469,"p95":3902.7246227036676,"p99":3916.4324046752017}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:24+00","p50":3682.455836625,"p95":3906.8168171603943,"p99":3926.1446072056137}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:25+00","p50":3714.160243875,"p95":3807.6549753764148,"p99":4243.0352156263}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:26+00","p50":3610.02310975,"p95":3848.6959780394754,"p99":3894.642236565126}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:27+00","p50":3367.9036615,"p95":4119.379643060286,"p99":4644.330091925518}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:28+00","p50":3708.9205795000007,"p95":4081.052548218066,"p99":4226.112703860503}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:29+00","p50":3754.872420625,"p95":4089.8264110353507,"p99":4113.270803625177}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:30+00","p50":3708.0099360000004,"p95":3991.776640260728,"p99":4455.903300192531}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:31+00","p50":3383.4435000000003,"p95":3849.007304410605,"p99":4262.2327881538495}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:32+00","p50":3154.5699707500003,"p95":3835.042006895425,"p99":4350.205721278855}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:33+00","p50":3052.694079,"p95":3824.373315358323,"p99":3840.765608320539}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:34+00","p50":3053.6514595,"p95":3863.4075231448646,"p99":4288.084960166306}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:35+00","p50":3454.366765875,"p95":3854.5260072592946,"p99":3870.431010146133}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:36+00","p50":3349.2670418750004,"p95":3856.044178251574,"p99":3871.8554580906725}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:37+00","p50":2747.319217,"p95":3721.041553761591,"p99":4346.134658214142}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:38+00","p50":3562.0915815,"p95":3863.899897610549,"p99":3962.8231215276082}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:39+00","p50":3340.3824335,"p95":4105.732637240895,"p99":4537.307260474404}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:40+00","p50":3256.706603,"p95":4000.5101709202127,"p99":4043.3421367043857}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:41+00","p50":3905.6795065,"p95":4012.1399818476866,"p99":4552.032211254334}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:42+00","p50":2991.6502903749997,"p95":4007.1312864526926,"p99":4019.095439462803}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:43+00","p50":3469.3386123749997,"p95":4071.813817585642,"p99":4095.600159799384}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:44+00","p50":3585.683851375,"p95":3941.489350639803,"p99":4638.135281574264}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:45+00","p50":3856.8403385,"p95":4017.0796890671127,"p99":4035.478505864053}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:46+00","p50":2940.5078195,"p95":3773.93889669586,"p99":4474.156302422608}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:47+00","p50":3591.5718561250005,"p95":3675.5968513814437,"p99":3687.858220286442}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:48+00","p50":3440.56427075,"p95":3825.30946438636,"p99":3831.5691005301596}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:49+00","p50":3584.4532115,"p95":3933.373878956428,"p99":4551.169619022185}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:50+00","p50":3136.814494,"p95":3932.325895534097,"p99":3978.9840594289494}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:51+00","p50":3839.042983,"p95":4012.906642236581,"p99":4028.0612456091694}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:52+00","p50":3292.715005125,"p95":4047.4743553852095,"p99":4506.683836180644}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:53+00","p50":3041.5506832499996,"p95":3698.9645644169636,"p99":3731.5143766242763}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:54+00","p50":3172.3841285,"p95":3737.4394148050847,"p99":4298.150013815507}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:55+00","p50":3255.35638075,"p95":3780.890340530244,"p99":3836.7153343961686}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:56+00","p50":3769.6480215,"p95":3906.710680721155,"p99":4483.35792025628}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:57+00","p50":3323.076683625,"p95":3971.6037864170125,"p99":3980.0865558853584}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:58+00","p50":3767.468596,"p95":3907.71721839526,"p99":3925.3024351791532}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:47:59+00","p50":3499.679782375,"p95":3819.6672460781165,"p99":4417.469959643172}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:00+00","p50":3002.176713375,"p95":3693.7053988896155,"p99":4238.745990164876}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:01+00","p50":3302.62677175,"p95":3629.479931922343,"p99":4055.1999496772714}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:02+00","p50":3446.0596255,"p95":3641.6219839969945,"p99":3955.957306440297}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:03+00","p50":3145.1932912499997,"p95":3747.861855413769,"p99":3784.522857660116}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:04+00","p50":3553.2062685,"p95":3800.8123090271306,"p99":4140.571545203362}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:05+00","p50":3322.535819625,"p95":3704.5634204281896,"p99":4159.703865871857}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:06+00","p50":3474.0307905,"p95":3731.9466671621703,"p99":3757.072692490649}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:07+00","p50":3392.8686883749997,"p95":3518.9910830001354,"p99":3866.564320280062}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:08+00","p50":3373.43653625,"p95":3511.578938210498,"p99":3522.9941011052106}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:09+00","p50":3345.43449775,"p95":3479.4378144656666,"p99":3522.2854153032017}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:10+00","p50":3357.59586725,"p95":3576.1345199928182,"p99":3761.552923722235}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:11+00","p50":3468.3278147499996,"p95":3644.000904716089,"p99":3883.3101071339574}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:12+00","p50":3324.567246,"p95":3800.920223985615,"p99":3816.8383981254765}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:13+00","p50":3500.2188832500005,"p95":3863.702652811264,"p99":3875.4692275182606}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:14+00","p50":3313.0407750000004,"p95":3623.44082229553,"p99":3950.342766671551}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:15+00","p50":3338.622219375,"p95":3619.557805209794,"p99":3673.637735772689}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:16+00","p50":3373.902023875,"p95":3497.1974045560078,"p99":3841.497829299298}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:17+00","p50":3317.09507925,"p95":3391.9921432527067,"p99":3420.3340025065354}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:18+00","p50":3311.7243965,"p95":3361.9973537788983,"p99":3454.4161370884676}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:19+00","p50":3335.23588525,"p95":3461.2207581588373,"p99":3691.649427846457}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:20+00","p50":3359.209798,"p95":3580.5994657890396,"p99":3920.047841423332}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:21+00","p50":3630.8800805,"p95":3754.0084381457214,"p99":3852.527506059455}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:22+00","p50":3826.23430475,"p95":3932.4185952253747,"p99":4041.939861703111}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:23+00","p50":3896.059774125,"p95":3997.888502550355,"p99":4263.1546214767195}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:24+00","p50":3857.9951355000003,"p95":4083.4085393344176,"p99":4118.38282436264}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:25+00","p50":3666.2752695,"p95":3993.0744810878014,"p99":4321.443113518581}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:26+00","p50":3618.1971825,"p95":3925.67628222143,"p99":4017.8322784658035}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:27+00","p50":3668.424494375,"p95":4015.2681303077193,"p99":4031.4518597169795}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:28+00","p50":3396.178513625,"p95":4008.1939419905834,"p99":4056.5644947471746}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:29+00","p50":3675.348331125,"p95":3937.4458658224294,"p99":3963.640467562859}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:30+00","p50":2860.82657775,"p95":3969.4403146487057,"p99":4558.860451585419}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:31+00","p50":3498.0901358749998,"p95":3950.723025816738,"p99":4682.539931651514}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:32+00","p50":3437.0636860000004,"p95":3953.7791773167855,"p99":3981.8305256421213}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:33+00","p50":3476.12017525,"p95":3928.6711137905427,"p99":3946.874205662733}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:34+00","p50":3661.492361,"p95":3846.9057351578467,"p99":4262.819813278351}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:35+00","p50":3355.85540375,"p95":3736.4332278985094,"p99":3750.915822079319}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:36+00","p50":3233.2034591250003,"p95":3951.7425175009553,"p99":3993.1856708201753}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:37+00","p50":3800.4993665,"p95":3946.3321681134803,"p99":3985.391349313882}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:38+00","p50":3287.240389125,"p95":3945.021939125961,"p99":3966.8105463573625}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:39+00","p50":3575.001732625,"p95":3959.5031184143363,"p99":4696.079762680968}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:40+00","p50":3396.9680857500002,"p95":3918.634922321576,"p99":4637.412179995861}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:41+00","p50":3851.1025852499997,"p95":3968.6565944721588,"p99":4691.224198002512}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:42+00","p50":2797.266524875,"p95":3944.7275516470972,"p99":3989.944908917239}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:43+00","p50":3660.28423175,"p95":4102.048783108424,"p99":4794.09969528903}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:44+00","p50":3954.512190375,"p95":4160.514203324861,"p99":4176.879308783208}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:45+00","p50":3852.4050805000006,"p95":4038.9457962032934,"p99":4874.755939136424}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:46+00","p50":2824.267112875,"p95":4059.3050005077116,"p99":4835.678668334291}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:47+00","p50":3819.2327257499996,"p95":4191.833404942792,"p99":5028.740161437212}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:48+00","p50":4058.3494232499997,"p95":4194.567093845974,"p99":4316.967725250379}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:49+00","p50":3279.54101275,"p95":4274.192831482855,"p99":4287.129614157831}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:50+00","p50":2864.5143535,"p95":4320.473854940418,"p99":5115.204164967999}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:51+00","p50":2531.658776,"p95":4541.40959193734,"p99":4795.498330090747}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:52+00","p50":2790.7170889999998,"p95":4794.937180091777,"p99":5076.054592071059}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:53+00","p50":4484.43504475,"p95":4596.523638227661,"p99":5591.165951289304}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:54+00","p50":4047.0662356250004,"p95":4555.248436140677,"p99":4570.06905514959}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:55+00","p50":3755.2556135,"p95":4433.9291225948455,"p99":4459.884110463858}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:56+00","p50":3291.8214485,"p95":4154.7740541422945,"p99":5010.443090921662}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:57+00","p50":3697.5069646250004,"p95":4256.7914560234785,"p99":4343.916485057212}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:58+00","p50":4252.0938235,"p95":4392.635449054446,"p99":4446.514051829006}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:48:59+00","p50":2875.6070028750005,"p95":4341.992869612838,"p99":4361.056814049542}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:00+00","p50":4209.7188504999995,"p95":4360.775583285486,"p99":5228.500527539152}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:01+00","p50":3971.6690415000003,"p95":4210.142298870083,"p99":4239.761555725617}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:02+00","p50":3232.18044775,"p95":3969.156104601209,"p99":4391.967301564991}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:03+00","p50":2878.136419625,"p95":3634.1062081286755,"p99":3641.21458917242}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:04+00","p50":2711.25419125,"p95":3713.097487837418,"p99":4263.139064133735}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:05+00","p50":3681.8437667499998,"p95":3851.1317951841643,"p99":4334.595286140082}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:06+00","p50":3117.4183461250004,"p95":3949.3956378772805,"p99":4515.444951831213}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:07+00","p50":3116.670551,"p95":4056.939254452862,"p99":4669.649252388866}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:08+00","p50":3472.3060823749997,"p95":4061.2533995753806,"p99":4818.902148104596}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:09+00","p50":3330.502269,"p95":3978.5626739632917,"p99":3990.787802936707}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:10+00","p50":3181.62138375,"p95":3855.119764512755,"p99":3864.882285875126}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:11+00","p50":3027.213356,"p95":4012.1705430168076,"p99":4559.2271997489925}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:12+00","p50":2971.219701875,"p95":4032.573415549307,"p99":4055.323414553824}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:13+00","p50":3180.6319685,"p95":4085.827620267112,"p99":4139.615403856628}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:14+00","p50":3092.784964125,"p95":4144.97394111496,"p99":4172.816364024464}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:15+00","p50":3133.5100475,"p95":4205.309328377183,"p99":4931.282744903501}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:16+00","p50":2886.91643225,"p95":4070.625581428514,"p99":4820.42027786703}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:17+00","p50":2737.621941625,"p95":3919.426511517052,"p99":4534.976847202906}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:18+00","p50":3565.4874222500002,"p95":3877.9548295261197,"p99":4596.34091409585}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:19+00","p50":3785.93115225,"p95":3873.9298346991395,"p99":3883.024393589695}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:20+00","p50":3283.248855,"p95":3930.7349280266094,"p99":3949.3289386234605}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:21+00","p50":3411.3625847499998,"p95":4088.39992755362,"p99":4598.581176823332}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:22+00","p50":3425.986636,"p95":4079.337359426509,"p99":4101.082843720764}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:23+00","p50":3845.2020199999997,"p95":3971.7152116123075,"p99":3982.600029397474}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:24+00","p50":3496.654479,"p95":3925.4609564516104,"p99":4457.828409755108}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:25+00","p50":2894.38199325,"p95":3881.2951399573594,"p99":3904.2295951999654}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:26+00","p50":3157.833692875,"p95":4046.050507289744,"p99":4783.090651269324}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:27+00","p50":3721.2909855000003,"p95":4130.304988412767,"p99":4842.550284018505}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:28+00","p50":3624.0202295,"p95":4340.344014574839,"p99":4960.080635533408}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:29+00","p50":3840.716517,"p95":4329.671105787081,"p99":4343.211122868736}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:30+00","p50":2990.029993125,"p95":4254.981680854623,"p99":5092.98825507415}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:31+00","p50":3825.8937426250004,"p95":4135.403359231585,"p99":4165.738626013905}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:32+00","p50":3417.0752967500002,"p95":4065.704051457041,"p99":4803.926031736047}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:33+00","p50":3426.740742,"p95":3931.736544834822,"p99":3953.41291818029}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:34+00","p50":3697.1244859999997,"p95":4105.348642196078,"p99":4238.026372213619}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:35+00","p50":3928.7785667499998,"p95":4123.67627571587,"p99":4783.692454775356}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:36+00","p50":3827.1003327500002,"p95":4012.654090446311,"p99":4019.24922856349}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:37+00","p50":3548.3124766250003,"p95":3899.4539109892194,"p99":4473.233707959822}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:38+00","p50":3363.8105002499997,"p95":3787.7317155705027,"p99":3883.480625227011}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:39+00","p50":3299.3148065,"p95":3797.784997172582,"p99":4309.530456985097}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:40+00","p50":3327.0245925,"p95":3884.824357914249,"p99":4317.86221026784}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:41+00","p50":3903.9679647499997,"p95":4014.7262001220383,"p99":4482.501211342505}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:42+00","p50":3096.70145975,"p95":4036.1825198040247,"p99":4105.90270070921}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:43+00","p50":3198.936430625,"p95":3937.3076656711505,"p99":5221.23817342346}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:44+00","p50":3562.689916125,"p95":4047.066331844918,"p99":4474.312752779149}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:45+00","p50":3859.224511,"p95":4010.974931327141,"p99":4033.2236231374536}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:46+00","p50":3080.1868507500003,"p95":3911.995275588331,"p99":3933.6640307433827}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:47+00","p50":3561.0423982499997,"p95":3937.69773122219,"p99":3971.6494379322626}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:48+00","p50":3715.2064286250006,"p95":4006.9934115527312,"p99":4106.8352083424115}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:49+00","p50":3378.0730049999997,"p95":4128.562882567588,"p99":4750.152678160591}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:50+00","p50":3826.22515225,"p95":4035.905366753853,"p99":4052.291523768325}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:51+00","p50":3658.3106184999997,"p95":4161.570337300816,"p99":4292.436186423427}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:52+00","p50":4135.119792,"p95":4334.576833445366,"p99":4941.72899585596}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:53+00","p50":4125.15305225,"p95":4229.4534510509,"p99":4273.216873617721}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:54+00","p50":3659.6921030000003,"p95":4252.902081874464,"p99":4712.979499835497}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:55+00","p50":3737.2648875000004,"p95":4220.479111222805,"p99":4234.785473902884}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:56+00","p50":3916.561315,"p95":4099.639985068733,"p99":4307.546090981873}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:57+00","p50":3876.284288,"p95":4079.2998214405466,"p99":4283.655437753157}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:58+00","p50":3689.71096,"p95":4345.389959044444,"p99":4884.211147842788}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:49:59+00","p50":3756.3526355000004,"p95":4432.744244575211,"p99":4914.3119163488045}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:00+00","p50":3513.26192725,"p95":4504.069601330679,"p99":5072.48415578261}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:01+00","p50":3970.63598325,"p95":4408.102357919537,"p99":5017.21289579997}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:02+00","p50":3590.6984136250003,"p95":4417.743753736366,"p99":5054.1295939645115}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:03+00","p50":3533.3595105000004,"p95":4089.48015999007,"p99":4100.733086511724}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:04+00","p50":3499.83775925,"p95":3930.7548480436944,"p99":4596.729775850812}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:05+00","p50":2591.0216535,"p95":4010.908790518728,"p99":4660.5573729094895}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:06+00","p50":3196.04450275,"p95":4059.976609592216,"p99":4085.5730730701466}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:07+00","p50":2704.0438510000004,"p95":4101.612066604574,"p99":4124.131786379134}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:08+00","p50":3488.8738625,"p95":4196.742113946957,"p99":5028.911528926803}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:09+00","p50":3985.928569875,"p95":4275.718181670776,"p99":5078.650898291466}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:10+00","p50":4065.409647375,"p95":4205.175735876238,"p99":5000.0748266533365}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:11+00","p50":2634.3936272500005,"p95":4093.0155831891843,"p99":4684.846596206195}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:12+00","p50":2625.58744425,"p95":4032.303247707032,"p99":4594.359487348326}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:13+00","p50":2741.749093875,"p95":3898.1429940562807,"p99":3910.9491055392486}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:14+00","p50":3727.6257315000003,"p95":3782.960846993162,"p99":3808.7294246469696}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:15+00","p50":3638.7184925,"p95":3918.7755566679466,"p99":3936.3960197367487}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:16+00","p50":3239.958539625,"p95":3884.5150767138484,"p99":3914.540387826057}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:17+00","p50":3548.443851125,"p95":3836.5946851480385,"p99":3906.9658595284827}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:18+00","p50":3264.645327625,"p95":4007.4979306462587,"p99":4572.342906816209}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:19+00","p50":3196.29390575,"p95":3981.7784995646984,"p99":4442.304122221695}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:20+00","p50":3730.22699175,"p95":4102.7535776841305,"p99":4144.287473273133}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:21+00","p50":4004.5302945000003,"p95":4120.327948984573,"p99":4137.204017442332}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:22+00","p50":3068.95017375,"p95":3938.5930539402375,"p99":4519.826354002909}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:23+00","p50":3563.9140829999997,"p95":3854.5693908422186,"p99":3879.2769285369263}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:24+00","p50":3703.1531452500003,"p95":4011.825015834928,"p99":4029.3391501707747}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:25+00","p50":3783.1856685000002,"p95":4049.495317865687,"p99":4058.475176634587}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:26+00","p50":3342.051540375,"p95":3752.1227671509155,"p99":3833.4945764767194}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:27+00","p50":3407.42258525,"p95":3863.270349539214,"p99":3885.734088911721}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:28+00","p50":3594.19765275,"p95":3812.1223323606764,"p99":3870.707716460747}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:29+00","p50":3676.366451,"p95":3907.557642208547,"p99":4232.940088108336}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:30+00","p50":3690.989141625,"p95":3774.897195736287,"p99":3786.1469128515614}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:31+00","p50":3567.1775935,"p95":3865.4670633032997,"p99":3881.79617298734}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:32+00","p50":3478.189039375,"p95":3802.4093095020353,"p99":3819.2204989052448}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:33+00","p50":3259.860707,"p95":3812.4640870957464,"p99":4234.380976033146}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:34+00","p50":3393.659735,"p95":3783.711112012714,"p99":4254.842167104444}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:35+00","p50":3551.89613925,"p95":3673.439322505545,"p99":3692.724417479886}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:36+00","p50":3569.5071616249998,"p95":3682.6274750412,"p99":3707.1260032176438}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:37+00","p50":3476.3557465000004,"p95":3640.8781875287527,"p99":3714.707033451789}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:38+00","p50":3454.147419375,"p95":3598.6176010950635,"p99":3809.0209880624857}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:39+00","p50":3522.8332655,"p95":3618.963979866225,"p99":3707.7346287859173}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:40+00","p50":3582.5636415,"p95":3669.1430594296226,"p99":3702.4738028545808}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:41+00","p50":3652.0945413749996,"p95":3762.5859908912107,"p99":3781.24847823841}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:42+00","p50":3466.673121875,"p95":3637.142103336092,"p99":3890.3872358528874}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:43+00","p50":3497.94171975,"p95":3533.2042318114654,"p99":3738.1493926212274}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:44+00","p50":3508.4726259999998,"p95":3646.526444855143,"p99":3667.2825380916424}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:45+00","p50":3396.36405125,"p95":3538.816400159257,"p99":3776.50222211284}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:46+00","p50":3254.974717625,"p95":3513.893297334054,"p99":3731.6792024515057}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:47+00","p50":3268.749916375,"p95":3387.145413921328,"p99":3404.357011624208}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:48+00","p50":3383.322949,"p95":3623.8193773184234,"p99":3652.3435990841854}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:49+00","p50":3588.8435586250002,"p95":3801.2640948371027,"p99":3815.5028449872534}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:50+00","p50":3404.94309725,"p95":3827.9370114902717,"p99":3850.4193897214245}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:51+00","p50":3343.8855488749996,"p95":3758.291339031178,"p99":4092.4101291656816}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:52+00","p50":3261.927202,"p95":3670.3702330429446,"p99":3691.2775140647236}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:53+00","p50":3261.8541216249996,"p95":3652.0098627553643,"p99":3660.3198985754893}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:54+00","p50":3478.436939125,"p95":3746.2673814092173,"p99":3766.6358441433395}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:55+00","p50":3475.4741594999996,"p95":3705.6999359363354,"p99":3732.4088072585123}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:56+00","p50":3758.308502,"p95":3892.913656375899,"p99":4106.288033797504}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:57+00","p50":3886.9247225,"p95":4137.196372076507,"p99":4233.2900471100365}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:58+00","p50":4113.7265065,"p95":4354.6278676973725,"p99":4366.885518648743}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:50:59+00","p50":4059.0590125,"p95":4346.974874381585,"p99":4394.104631837684}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:00+00","p50":4123.37521075,"p95":4387.214822127206,"p99":4438.837883425328}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:01+00","p50":3926.0875707500004,"p95":4170.5611225054345,"p99":4220.003983226582}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:02+00","p50":3696.944703,"p95":4054.587137581211,"p99":4081.494944812359}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:03+00","p50":3741.8522844999998,"p95":3908.473633150468,"p99":3940.9655623894887}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:04+00","p50":3701.8341924999995,"p95":3810.431997288846,"p99":3853.4613968023814}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:05+00","p50":3441.5654765,"p95":3840.9831300794935,"p99":3847.0397674905207}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:06+00","p50":3423.32559425,"p95":3671.9270311036644,"p99":3755.196880113621}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:07+00","p50":3264.451581375,"p95":3756.0761290999462,"p99":3778.0540778437517}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:08+00","p50":3467.5972604999997,"p95":3802.207919479238,"p99":4350.028048632911}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:09+00","p50":3444.660111,"p95":3942.890633540286,"p99":4370.434304390428}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:10+00","p50":3280.1068485,"p95":3933.3692331028024,"p99":4467.731255347531}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:11+00","p50":3250.368881875,"p95":3868.199785086028,"p99":4351.308156746706}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:12+00","p50":3321.1403105,"p95":3687.446163436718,"p99":4065.0332663395766}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:13+00","p50":3046.00306,"p95":3744.725148933969,"p99":3762.5470995541705}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:14+00","p50":3672.7166455,"p95":3782.32534452025,"p99":3790.807144066924}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:15+00","p50":3101.80108325,"p95":3759.923300643858,"p99":4206.637460863566}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:16+00","p50":2764.1411228750003,"p95":3812.789313705831,"p99":3856.2776094420174}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:17+00","p50":3431.910268,"p95":3978.8778096397778,"p99":4002.7938357729286}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:18+00","p50":3061.3433005,"p95":4042.509913976914,"p99":4672.121480232881}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:19+00","p50":2914.358311375,"p95":4137.517840366719,"p99":4733.270925988931}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:20+00","p50":3230.998125,"p95":4181.9778844851535,"p99":4236.864147371973}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:21+00","p50":3059.244083,"p95":4431.421298366366,"p99":4786.72715919501}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:22+00","p50":3515.550223,"p95":4553.746914573974,"p99":5277.816575173057}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:23+00","p50":4165.06144575,"p95":4527.264556149026,"p99":4642.685907728323}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:24+00","p50":4201.035113,"p95":4450.364714446303,"p99":5134.896671721409}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:25+00","p50":3682.3315184999997,"p95":4387.292216900938,"p99":4411.406879255306}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:26+00","p50":3806.356950625,"p95":4187.727844046704,"p99":4403.764548947687}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:27+00","p50":3268.5661465,"p95":4189.873293536976,"p99":4209.7171230666345}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:28+00","p50":3428.25178125,"p95":4382.35200563987,"p99":4528.267526430603}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:29+00","p50":4244.842198,"p95":4342.44718754857,"p99":4362.850603471947}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:30+00","p50":4065.88885325,"p95":4334.952437339282,"p99":4346.1323922059155}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:31+00","p50":3390.6674550000002,"p95":4141.870437860814,"p99":4155.109141130101}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:32+00","p50":3199.16686675,"p95":3986.8898236877026,"p99":4018.6279882801896}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:33+00","p50":3618.034507875,"p95":3919.2058186715376,"p99":4376.360015378662}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:34+00","p50":3435.9687875,"p95":3928.7586037908063,"p99":3937.60017667705}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:35+00","p50":3273.5060886250003,"p95":3895.657214812276,"p99":4465.138324078252}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:36+00","p50":3530.0161625,"p95":3803.072513919371,"p99":3821.3020919546007}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:37+00","p50":3034.456533,"p95":3886.8149875887734,"p99":4337.944350849963}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:38+00","p50":3077.53229975,"p95":3879.1877997284523,"p99":4400.246009910859}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:39+00","p50":3304.319939,"p95":3754.014611728622,"p99":3762.3601628304823}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:40+00","p50":3112.78696725,"p95":3934.444384207564,"p99":4356.852462352748}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:41+00","p50":3322.0370045,"p95":3812.4745357793186,"p99":4324.6569060350785}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:42+00","p50":3638.9799752500003,"p95":3742.740542137957,"p99":3760.29433452643}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:43+00","p50":3088.775197375,"p95":3976.29159942042,"p99":3998.79798672692}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:44+00","p50":3274.4298615,"p95":3944.110962997953,"p99":4516.696505337645}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:45+00","p50":2848.494659375,"p95":4055.593202779349,"p99":4076.744802345298}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:46+00","p50":3645.84357225,"p95":4121.85231423915,"p99":4775.472177574632}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:47+00","p50":3286.2593915,"p95":3908.4857709997764,"p99":4525.147640110204}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:48+00","p50":2681.76538275,"p95":3986.3831474236454,"p99":4472.626282499356}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:49+00","p50":3437.6264894999995,"p95":4044.5092973045485,"p99":4058.4148553204654}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:50+00","p50":3014.8825795000002,"p95":4029.4762800637445,"p99":4724.505633448908}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:51+00","p50":3005.2711415,"p95":3977.403469541694,"p99":4014.3210794334746}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:52+00","p50":3379.31126725,"p95":3982.2046183315874,"p99":4027.468221946945}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:53+00","p50":3712.479721625,"p95":4040.1148388476126,"p99":4496.623911478722}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:54+00","p50":3718.89844475,"p95":4230.619881895183,"p99":4711.987282355972}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:55+00","p50":3964.865779375,"p95":4072.760136725823,"p99":4151.878655709956}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:56+00","p50":3077.40314825,"p95":4232.695189004436,"p99":4947.952323778715}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:57+00","p50":3170.67126125,"p95":4346.471315506554,"p99":4469.607522176604}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:58+00","p50":3355.3561227500004,"p95":4393.926586385938,"p99":5217.568023956872}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:51:59+00","p50":3992.2074606250007,"p95":4413.441307777922,"p99":4450.374918238903}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:00+00","p50":3863.7554822500006,"p95":4357.51893065914,"p99":4390.185371618143}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:01+00","p50":3158.52223825,"p95":4322.8334968676145,"p99":5262.642909546411}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:02+00","p50":3104.935314375,"p95":4353.991860310992,"p99":4551.157199319195}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:03+00","p50":3076.9627450000003,"p95":4261.0790584145225,"p99":4267.34488166156}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:04+00","p50":3286.43663725,"p95":4350.789402611155,"p99":5111.04333314303}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:05+00","p50":2524.5997070000003,"p95":4398.434303963963,"p99":5201.043009315594}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:06+00","p50":2784.974779375,"p95":4274.987522544268,"p99":5215.592486864818}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:07+00","p50":3280.8217525,"p95":4290.417766468806,"p99":5027.47592207267}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:08+00","p50":3479.381598,"p95":4359.248166199043,"p99":4533.1745156120605}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:09+00","p50":3923.83701075,"p95":4392.219952953632,"p99":4422.566743556503}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:10+00","p50":4210.1626694999995,"p95":4343.508493885765,"p99":4365.68862116021}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:11+00","p50":3763.8135381250004,"p95":4247.119115237459,"p99":5004.04709354722}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:12+00","p50":4010.825891,"p95":4209.762184284947,"p99":5065.318898893201}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:13+00","p50":2800.9290485,"p95":4228.392842853189,"p99":5111.959961594619}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:14+00","p50":3033.0847495000003,"p95":4231.308935692112,"p99":4364.384085018588}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:15+00","p50":3040.677462625,"p95":4321.638187356486,"p99":5178.767803590393}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:16+00","p50":2532.653916125,"p95":4323.102500154561,"p99":4341.397396873927}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:17+00","p50":3418.7156195,"p95":4157.945362679695,"p99":5034.6335030834}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:18+00","p50":2932.276009,"p95":4088.368938225615,"p99":4174.070528711085}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:19+00","p50":2874.61126,"p95":4086.4235300252144,"p99":4109.979552878586}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:20+00","p50":3594.3187909999997,"p95":4139.204867922915,"p99":4800.541623858422}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:21+00","p50":4117.0529102499995,"p95":4237.946138231094,"p99":4252.408356966955}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:22+00","p50":3220.45785625,"p95":4332.985400281049,"p99":5185.32882526319}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:23+00","p50":3308.22097675,"p95":4298.633021555444,"p99":4313.417781602096}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:24+00","p50":4095.2177659999998,"p95":4252.930459736743,"p99":4897.330592732889}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:25+00","p50":3950.018024125,"p95":4109.160450707072,"p99":4179.398785873885}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:26+00","p50":4002.68078975,"p95":4134.66360315441,"p99":4978.697721366676}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:27+00","p50":3596.350757,"p95":4179.750166423774,"p99":4197.142314859637}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:28+00","p50":3628.67160325,"p95":4212.424477426786,"p99":4340.640498543789}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:29+00","p50":3675.4430712500002,"p95":4306.338899578416,"p99":4897.5834694781415}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:30+00","p50":3948.917016,"p95":4224.946901013242,"p99":4826.579375171393}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:31+00","p50":3469.487421,"p95":3930.730237587129,"p99":3953.0250260484654}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:32+00","p50":3083.863044375,"p95":3726.932729897453,"p99":4150.21705999666}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:33+00","p50":3105.3508348749997,"p95":3763.605474876408,"p99":3781.9620404498773}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:34+00","p50":3782.59313275,"p95":3922.929815013445,"p99":3955.608686722959}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:35+00","p50":3041.1746569999996,"p95":3992.735835832775,"p99":4014.0475872033844}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:36+00","p50":2928.4038255,"p95":3986.372190269647,"p99":4682.538605057886}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:37+00","p50":3315.9394541250003,"p95":4038.5654134206607,"p99":4629.928584766022}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:38+00","p50":3574.1275917499997,"p95":4180.544871879013,"p99":4845.896661036783}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:39+00","p50":3240.27391725,"p95":4240.563013195317,"p99":4272.258487495628}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:40+00","p50":3340.4365067500003,"p95":4213.633633789232,"p99":5086.259622217498}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:41+00","p50":3932.059367,"p95":4190.350603140117,"p99":4762.05930040147}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:42+00","p50":3369.11230475,"p95":4297.1355562636245,"p99":4345.497632587126}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:43+00","p50":3350.4895834999998,"p95":4095.2068079199353,"p99":4595.247552232674}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:44+00","p50":3193.496433625,"p95":3925.668495990337,"p99":4484.352395698373}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:45+00","p50":3273.655376375,"p95":3780.691578554816,"p99":4408.096976133244}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:46+00","p50":3355.88600275,"p95":3725.37072898849,"p99":4352.801956759418}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:47+00","p50":2905.1674319999997,"p95":3707.48949344503,"p99":4439.3140812515785}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:48+00","p50":2875.55426825,"p95":3716.474287900196,"p99":3736.076232690663}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:49+00","p50":3255.7483272500003,"p95":3807.5877490683274,"p99":4334.366364494583}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:50+00","p50":3226.4537742499997,"p95":3936.236363489983,"p99":4417.472319034705}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:51+00","p50":3122.848993,"p95":4110.143453764503,"p99":4242.247362830797}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:52+00","p50":3501.5904902499997,"p95":4181.18177834839,"p99":4769.722095780128}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:53+00","p50":3661.92717225,"p95":4264.378009351425,"p99":4883.177631505583}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:54+00","p50":3295.09692925,"p95":4228.172205112231,"p99":4899.515494234861}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:55+00","p50":3319.32574475,"p95":4176.045701368227,"p99":4755.6085605913495}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:56+00","p50":3335.887191,"p95":4224.28904489879,"p99":4932.960077216013}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:57+00","p50":3897.1141165000004,"p95":4270.990775052023,"p99":4281.2973386484955}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:58+00","p50":3975.1307445,"p95":4413.739140334941,"p99":5036.041371318865}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:52:59+00","p50":3692.8683415,"p95":4431.951627514288,"p99":4444.714869964452}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:00+00","p50":3126.263921125,"p95":4311.186180935215,"p99":4329.273697889732}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:01+00","p50":3671.3140602500002,"p95":4180.525943455105,"p99":4933.846315105341}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:02+00","p50":3373.8056145,"p95":3877.4915476187016,"p99":3899.351167566413}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:03+00","p50":3415.61749175,"p95":3845.133868009045,"p99":4349.509725820417}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:04+00","p50":3477.8516411250002,"p95":3927.2647210829596,"p99":3950.1284124844856}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:05+00","p50":2962.3526026249997,"p95":3978.6388541455294,"p99":4003.262500709146}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:06+00","p50":3666.578915,"p95":4028.6996556479608,"p99":4081.0698658036845}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:07+00","p50":3516.624269,"p95":4167.613366251304,"p99":4171.237176347041}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:08+00","p50":3021.2191505,"p95":4250.5297114260475,"p99":4813.097225902049}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:09+00","p50":3898.4550355,"p95":4559.138073097694,"p99":4670.2603107225195}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:10+00","p50":3140.226021,"p95":4616.716057357306,"p99":5290.30808912329}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:11+00","p50":3678.6606187499997,"p95":4620.271523715263,"p99":5402.347656022516}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:12+00","p50":3062.199697,"p95":4523.969735484652,"p99":4532.016006389976}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:13+00","p50":3396.5344324999996,"p95":4439.9423698904575,"p99":4467.539408899662}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:14+00","p50":3052.3870435,"p95":3928.648581655456,"p99":4645.362865194164}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:15+00","p50":2708.2288234999996,"p95":3777.240485622151,"p99":3797.1860364055383}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:16+00","p50":3037.5108812500002,"p95":3695.8751140751338,"p99":3723.76596759219}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:17+00","p50":3306.06396375,"p95":3734.4796313950646,"p99":4421.228064400251}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:18+00","p50":2779.879696125,"p95":3724.005532462583,"p99":4400.081171054592}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:19+00","p50":2988.73613975,"p95":3780.202494304243,"p99":4381.499812596303}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:20+00","p50":3189.05869725,"p95":4025.1343597214886,"p99":4043.709645650562}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:21+00","p50":3636.6867332499996,"p95":4057.886428668622,"p99":4766.312901069349}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:22+00","p50":2683.18713975,"p95":4042.4122553528346,"p99":4758.252804008703}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:23+00","p50":3317.6708095,"p95":4146.22463652978,"p99":4222.748071803879}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:24+00","p50":2953.0242088749997,"p95":4322.087190279485,"p99":5029.6327175880015}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:25+00","p50":3542.38324725,"p95":4236.267924509322,"p99":5032.502154729664}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:26+00","p50":2781.7532119999996,"p95":4260.95399625087,"p99":4276.3693778317565}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:27+00","p50":3672.692691375,"p95":4301.356214906413,"p99":5085.310337381889}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:28+00","p50":3431.0196645,"p95":4376.116703265064,"p99":4476.033953068206}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:29+00","p50":3364.9333957500003,"p95":4509.319483241294,"p99":4562.7246175558585}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:30+00","p50":3601.421343,"p95":4544.521339320717,"p99":5415.593298193201}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:31+00","p50":3114.98580175,"p95":4514.549393577664,"p99":5428.428283464027}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:32+00","p50":3270.7029062499996,"p95":4478.151380858793,"p99":4505.028384884117}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:33+00","p50":4251.44571125,"p95":4334.127302934853,"p99":5213.590554125587}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:34+00","p50":3955.9433928749995,"p95":4274.6316377061385,"p99":5160.483470282863}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:35+00","p50":2730.231035,"p95":4224.812471302139,"p99":5155.012997445399}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:36+00","p50":2740.841240625,"p95":4169.845496235683,"p99":5002.825912383365}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:37+00","p50":3466.91932225,"p95":4098.298689206168,"p99":4851.962881066275}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:38+00","p50":3195.3297518750005,"p95":3841.990032530735,"p99":4405.51448778487}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:39+00","p50":3486.654129125,"p95":3977.3893171086647,"p99":4615.822114819283}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:40+00","p50":3705.3999887500004,"p95":3965.480967176212,"p99":4609.020513642065}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:41+00","p50":2702.469621375,"p95":3792.5891959802334,"p99":3806.40040763914}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:42+00","p50":2575.7282647499997,"p95":3763.1454708832025,"p99":3774.3029380155776}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:43+00","p50":3737.7915315,"p95":3802.67329759068,"p99":4463.774103762421}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:44+00","p50":3702.6404815,"p95":3789.0663179148246,"p99":4415.640611074117}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:45+00","p50":3134.237647625,"p95":3892.566104017494,"p99":4460.287838382341}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:46+00","p50":2646.21916275,"p95":4058.9403737926723,"p99":4611.053050609773}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:47+00","p50":3961.461541,"p95":4080.059711456858,"p99":4884.369404328555}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:48+00","p50":4109.922271375,"p95":4194.836755133191,"p99":4897.875881977797}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:49+00","p50":3488.3972546249997,"p95":4172.536341089441,"p99":4194.935515164205}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:50+00","p50":2921.39000925,"p95":4065.711708353357,"p99":4763.266214436202}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:51+00","p50":3724.5597823750004,"p95":4078.4138190072586,"p99":4823.817877650476}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:52+00","p50":4028.0158118749996,"p95":4217.403043257193,"p99":4232.79783776757}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:53+00","p50":3344.861666875,"p95":4248.092933619697,"p99":4968.780911107461}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:54+00","p50":3035.9868079999997,"p95":4145.509347159617,"p99":4853.768768969936}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:55+00","p50":3334.8089202499996,"p95":4089.4243887576863,"p99":4834.68591370573}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:56+00","p50":3743.243306625,"p95":4253.358183471146,"p99":4268.750615562093}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:57+00","p50":2718.127735,"p95":4425.924394794082,"p99":4595.441200773346}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:58+00","p50":3928.84313,"p95":4465.8216090961,"p99":4489.043432457666}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:53:59+00","p50":3075.6364584999997,"p95":4441.023837900472,"p99":4467.070294053782}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:54:00+00","p50":3362.0793553333333,"p95":4145.17991284587,"p99":4684.31318532596}, + {"metric_name":"session_add_session_duration","timestamp":"2025-02-25 14:54:01+00","p50":2652.321399,"p95":2675.0949608567266,"p99":2675.359307} +] diff --git a/docs/docs/apis/introduction.mdx b/docs/docs/apis/introduction.mdx index fe8b50bdb7..e05a7e84b3 100644 --- a/docs/docs/apis/introduction.mdx +++ b/docs/docs/apis/introduction.mdx @@ -36,7 +36,7 @@ Accessing the ZITADEL APIs through a service user might require additional steps - Standard and reserved [scopes reference](/docs/apis/openidoauth/scopes) - Standard, custom, and reserved [claims reference](/docs/apis/openidoauth/claims) -The [OIDC Playground](/docs/apis/openidoauth/authrequest) is for testing OpenID authentication requests and their parameters. +The [OIDC Playground](https://zitadel.com/playgrounds/oidc) is for testing OpenID authentication requests and their parameters. ### SAML 2.0 @@ -70,7 +70,7 @@ ZITADEL APIs were organized by UseCase/Context, such as Auth API for authenticat This led to confusion about which API to use, particularly for requests that could be useful across multiple APIs but with different filters. For instance, SearchUsers on an Instance Level or on an Organization Level. -To address this issue, ZITADEL is migrating to a [resource-based API](#zitadel-apis-resource-based). +To address this issue, ZITADEL is migrating to a [resource-based API](#zitadel-apis-resource-based). ::: @@ -224,7 +224,6 @@ Definition: - ## API definitions Each service's proto definition is located in the source control on GitHub. @@ -270,8 +269,8 @@ As you can see the `GetMyUser` function is also available as a REST service unde In the table below you can see the URI of those calls. -| Service | URI | -| :------ | :-------------------------------------------------- | +| Service | URI | +| :------ | :---------------------------------------------------- | | REST | $ZITADEL_DOMAIN/auth/v1/users/me | | GRPC | $ZITADEL_DOMAIN/zitadel.auth.v1.AuthService/GetMyUser | @@ -281,7 +280,7 @@ ZITADEL hosts everything under a single domain: `{instance}.zitadel.cloud` or yo The domain is used as the OIDC issuer and as the base url for the gRPC and REST APIs, the Login and Console UI, which you'll find under `{your_domain}/ui/console/`. -Are you self-hosting and having troubles with *Instance not found* errors? [Check out this page](/docs/self-hosting/manage/custom-domain). +Are you self-hosting and having troubles with _Instance not found_ errors? [Check out this page](/docs/self-hosting/manage/custom-domain). ## API path prefixes diff --git a/docs/docs/apis/openidoauth/claims.md b/docs/docs/apis/openidoauth/claims.md index 4129806aef..b7424aaf1d 100644 --- a/docs/docs/apis/openidoauth/claims.md +++ b/docs/docs/apis/openidoauth/claims.md @@ -110,7 +110,6 @@ ZITADEL reserves some claims to assert certain data. Please check out the [reser | urn:zitadel:iam:org:domain:primary:\{domainname} | `{"urn:zitadel:iam:org:domain:primary": "acme.ch"}` | This claim represents the primary domain of the organization the user belongs to. | | urn:zitadel:iam:org:project:roles | `{"urn:zitadel:iam:org:project:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on the current project (where your client belongs to). | | urn:zitadel:iam:org:project:\{projectid}:roles | `{"urn:zitadel:iam:org:project:id3:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on a specific project. | -| urn:zitadel:iam:roles:\{rolename} | TBA | TBA | | urn:zitadel:iam:user:metadata | `{"urn:zitadel:iam:user:metadata": [ {"key": "VmFsdWU=" } ] }` | The metadata claim will include all metadata of a user. The values are base64 encoded. | | urn:zitadel:iam:user:resourceowner:id | `{"urn:zitadel:iam:user:resourceowner:id": "orgid"}` | This claim represents the id of the resource owner organisation of the user. | | urn:zitadel:iam:user:resourceowner:name | `{"urn:zitadel:iam:user:resourceowner:name": "ACME"}` | This claim represents the name of the resource owner organisation of the user. | diff --git a/docs/docs/apis/openidoauth/scopes.md b/docs/docs/apis/openidoauth/scopes.md index a43f18b2f9..86f9769cab 100644 --- a/docs/docs/apis/openidoauth/scopes.md +++ b/docs/docs/apis/openidoauth/scopes.md @@ -30,12 +30,11 @@ In addition to the standard compliant scopes we utilize the following scopes. | `urn:zitadel:iam:org:projects:roles` | `urn:zitadel:iam:org:projects:roles` | By using this scope a client can request the claim `urn:zitadel:iam:org:project:{projectid}:roles` to be asserted for each requested project. All projects of the token audience, requested by the `urn:zitadel:iam:org:project:id:{projectid}:aud` scopes will be used. | | `urn:zitadel:iam:org:id:{id}` | `urn:zitadel:iam:org:id:178204173316174381` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization. If the organization does not exist a failure is displayed. It will assert the `urn:zitadel:iam:user:resourceowner` claims. | | `urn:zitadel:iam:org:domain:primary:{domainname}` | `urn:zitadel:iam:org:domain:primary:acme.ch` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization and the username is suffixed by the provided domain. If the organization does not exist a failure is displayed | -| `urn:zitadel:iam:role:{rolename}` | | | | `urn:zitadel:iam:org:roles:id:{orgID}` | `urn:zitadel:iam:org:roles:id:178204173316174381` | This scope can be used one or more times to limit the granted organization IDs in the returned roles. Unknown organization IDs are ignored. When this scope is not used, all granted organizations are returned inside the roles.[^1] | | `urn:zitadel:iam:org:project:id:{projectid}:aud` | `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access token | | `urn:zitadel:iam:org:project:id:zitadel:aud` | `urn:zitadel:iam:org:project:id:zitadel:aud` | By adding this scope, the ZITADEL project ID will be added to the audience of the access token | | `urn:zitadel:iam:user:metadata` | `urn:zitadel:iam:user:metadata` | By adding this scope, the metadata of the user will be included in the token. The values are base64 encoded. | -| `urn:zitadel:iam:user:resourceowner` | `urn:zitadel:iam:user:resourceowner` | By adding this scope, the resourceowner (id, name, primary_domain) of the user will be included in the token. | +| `urn:zitadel:iam:user:resourceowner` | `urn:zitadel:iam:user:resourceowner` | By adding this scope: id, name and primary_domain of the resource owner (the users organization) will be included in the token. | | `urn:zitadel:iam:org:idp:id:{idp_id}` | `urn:zitadel:iam:org:idp:id:76625965177954913` | By adding this scope the user will directly be redirected to the identity provider to authenticate. Make sure you also send the primary domain scope if a custom login policy is configured. Otherwise the system will not be able to identify the identity provider. | [^1]: `urn:zitadel:iam:org:roles:id:{orgID}` is not supported when the `oidcLegacyIntrospection` [feature flag](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) is enabled. diff --git a/docs/docs/apis/v3.mdx b/docs/docs/apis/v3.mdx deleted file mode 100644 index 202e907bd8..0000000000 --- a/docs/docs/apis/v3.mdx +++ /dev/null @@ -1,356 +0,0 @@ ---- -title: APIs V3 (Preview) ---- - -import DocCardList from "@theme/DocCardList"; -import CodeBlock from "@theme/CodeBlock"; -import ActionServiceProto from "!!raw-loader!./_v3_action_service.proto"; -import ActionExecutionProto from "!!raw-loader!./_v3_action_execution.proto"; -import ActionTargetProto from "!!raw-loader!./_v3_action_target.proto"; -import ActionSearchProto from "!!raw-loader!./_v3_action_search.proto"; -import IDPServiceProto from "!!raw-loader!./_v3_idp_service.proto"; -import IDPProto from "!!raw-loader!./_v3_idp.proto"; -import IDPSearchProto from "!!raw-loader!./_v3_idp_search.proto"; -import IDPGitLabProto from "!!raw-loader!./_v3_idp_gitlab.proto"; -import LanguageServiceProto from "!!raw-loader!./_v3_language_service.proto"; -import LanguageProto from "!!raw-loader!./_v3_language.proto"; -import ObjectProto from "!!raw-loader!./_v3_object.proto"; -import ResourceObjectProto from "!!raw-loader!./_v3_resource_object.proto"; -import SettingsObjectProto from "!!raw-loader!./_v3_settings_object.proto"; - -The APIs described in this section are currently either in _Preview_ stage or not implemented, yet. -Before using these APIs, pleases consider the [API release policy below](#api-release-policy) - -## We Appreciate your Help - -We invite you to... - -- ... [discuss the concept with the ZITADEL community on GitHub](https://github.com/zitadel/zitadel/discussions/8125). -- ... try the implementations and provide feedback [by filing issues on GitHub](https://github.com/zitadel/zitadel/issues/new/choose). - -## The Ideas behind the New V3 APIs - -The current ZITADEL _GA_ APIs are structured around contexts like System, Admin, Management, and Auth. -This structure leads to duplicate methods and makes it hard to find the right API for the right task. -Especially interacting with resources from multiple organizations is cumbersome. -Also, the APIs evolved over time, which lead to inconsistencies and a lack of flexibility in development. - -We address these issues with the following new API categories: - -- [Standard Resources](#standard-resources) -- [Reusable Resources](#reusable-resources) -- [Settings](#settings) - -The designs for the new API categories aim for the following improvements: - -### Service Structure - -Instead of structuring the API methods around contexts, new APIs are structured around resources and settings. -This means, eventually, we deprecate the old System-, Admin-, Management- and AuthAPIs in favor of User-, Action-, Language-, FeatureAPIs and so on. -This change makes it easier to find the right API for the right task, especially for multi-organization resources. -Also, it allows for faster development and independent versioning of the APIs. - -### Multitenancy Management and Consistency - -To improve managing and reusing resources and settings in multitenancy scenarios, we define some rules for the new APIs: - -- Single properties from default settings are overridable (patchable) in organizations. -- Some settings support user-defined custom properties that are also overridable in organizations. -- Improved experience with reusing resources in multiple organizations and instances. -- Resources are searchable over all organizations with a single call by default. - -### HTTP and gRPC Consistency - -To make the APIs more consistent and easier to use, we follow the same patterns in all Proto files. - -- Patching is favored over updating resources and settings. -- HTTP calls are mapped so that query parameters can be used as much as possible. We avoid the annotation `body: "*"`. -- For search performance, we enforce query limits. - -## Standard Resources - -Standard resources exist in exactly one context. -For example, a user is always assigned to exactly one organization. -Or one SMS provider is always assigned to exactly one instance. - -Standard resource methods behave like this: - -- Search request results can be scoped to a RequestContext. -- Search request results only contain results for which the requesting user has the necessary read permissions. -- Search requests are limited to 100 by default. The limit can be increased by the caller up to 1000 by default. -- Resource configurations are partially updatable. With HTTP, this is done via PATCH requests. If no changes were made, the response is successful. -- Status changes or other actions on resources with side effects are done via POST requests. Their HTTP path ends with the underscore prefixed action name. For example `POST /resources/users/{id}/_unlock`. - -For a full proto example, have a look at the [ZITADELActions service](#zitadelactions). - -## Reusable Resources - -Reusable resources are like standard resources but can be reused in multiple contexts. -For example, an external identity provider can be defined once on the instance. -Each organization within this instance can then choose to use this identity provider or not. - -Additionally to the methods described for standard resources, reusable have the following capabilities: - -Reusable resources have the same behavior as standard resources with the following additions: - -- Reusable resources can be created in a given context level (system, instance, org). -- For requests, that require a resource ID, no request context is needed. -- Reusable resources are available in child contexts, even if their state is _inactive_. -- The child context can control if an inherited resource should be active or inactive for itself using a state policy. -- In child contexts, the state policy of a reused resource is _inherit_ by default and can be changed to _activate_, _deactivate_ or back to _inherit_. -- In child contexts, a reused resources configuration is read-only. -- Child contexts can read at least the following properties of reused resources: - - ID - - name - - description - - state - - the state policy in the child context - - sequence - - last changed date - - parent context - - state in the immediate parent context. -- By default, search queries for reused resources return all resources from the given contexts, all inherited resources and all resources defined in all children contexts. - -Typically, a new resource is first designed and implemented as a non-reusable resource. -If the community sees a benefit in reusing the resource in multiple contexts, reusability is added to the resource. - -For a full proto example, have a look at the [ZITADELIdentityProviders service](#zitadelidentityproviders). - -## Resource Services - -All resource services by default support the following CRUD operations [as described above](#standard-resources-behavior). - -- Create -- Read (get, search) -- Patch (partially update, success on no changes) -- Delete - -### ZITADELActions - -- Standard CRUD methods for Targets -- Standard CRUD methods for Executions except the PutExecution method replaces the CreateExecution and PatchExecution methods - -Additional to the standard CRUD methods: - -- ListAvailableExecutionServices -- ListAvailableExecutionMethods -- ListAvailableExecutionFunctions - -
- action_service.proto - {ActionServiceProto} -
- -
- action_target.proto - {ActionTargetProto} -
- -
- action_execution.proto - {ActionExecutionProto} -
- -
- action_query.proto - {ActionSearchProto} -
- -### ZITADELUsers - -Standard CRUD methods - -### ZITADELUserSchemas - -Standard CRUD methods - -### ZITADELIdentityProviders - -- Standard CRUD and methods for all IDPs -- Resources have additional properties for reusability capabilities. - -
- idp_service.proto - {IDPServiceProto} -
- -
- idp.proto - {IDPProto} -
- -
- idp_search.proto - {IDPSearchProto} -
- -
- idp_gitlab.proto - {IDPGitLabProto} -
- -
- object.proto - {ObjectProto} -
- -
- resource_object.proto - {ResourceObjectProto} -
- -### ZITADELInstances - -Additional to the standard CRUD methods: - -- Limit (partial update of block and audit log retention) -- BulkLimit (partial update of block and audit log retention for multiple instances) - -### ZITADELOrganizations - -Additional to the standard CRUD methods: - -- SetAsInstanceDefault -- GetInstanceDefault - -### ZITADELDomains - -Additional to the standard CRUD methods: - -- SetAsPrimary -- Validate - -### ZITADELSessions - -Standard CRUD methods - -### ZITADELProjects - -Standard CRUD methods - -### ZITADELApps - -Standard CRUD methods - -### ZITADELMemberships - -The given context defines the organization, instance or system where the membership is created. -The context and the user ID together are unique. - -Additional to the standard CRUD methods: - -- ListAvailableRoles (context-aware) - -### ZITADELGrants - -- Standard CRUD methods for project grants -- Standard CRUD methods for user grants -- Standard CRUD methods for roles - -### ZITADELSMTPProviders - -Standard CRUD methods - -### ZITADELSMSProviders - -Standard CRUD methods - -## Settings - -Settings have no identity (ID) and are always context-aware. -They also don't have a state like active or inactive. -They only have properties that can be set and queried. -These properties are inherited to from parent-contexts (instance) to child-contexts (organization). - -Settings behave like this: - -- Setting and retrieving settings is always context-aware. By default, the context is the instance discovered by the requests _Host_ header. -- All settings properties can be partially overridden in child-contexts. -- All settings properties can be partially reset in child-contexts, so their values default to the parent contexts property values. -- All settings properties returned by queries contain the value and if it is inherited, the context where it is inherited from. - -For a full proto example, have a look at the [ZITADELLanguageSettings service](#zitadellanguagesettings). - -## Settings Services - -### ZITADELLanguageSettings - -Default language, restricted languages, supported languages - -
- language_service.proto - {LanguageServiceProto} -
- -
- language.proto - {LanguageProto} -
- -
- object.proto - {ObjectProto} -
- -
- settings_object.proto - {SettingsObjectProto} -
- -### ZITADELTextSettings - -Key-value pairs for localized login texts, previously known as login texts - -### ZITADELBrandingSettings - -Predefined branding settings and custom key-value pairs, previously known as label policy or branding settings - -### ZITADELLoginSettings - -Previously known as login policy - -### ZITADELLockoutSettings - -Previously known as lockout policy - -### ZITADELPasswordSettings - -Previously known as password complexity policy - -### ZITADELHelpSettings - -Previously known as legal and support settings or privacy policy - -### ZITADELDomainSettings - -Previously known as domain policy - -### ZITADELFeatureSettings - -Feature toggles - -Also contains disallow public org registrations on system and instance level. - -### ZITADELTemplatesSettings - -HTML and text templates for fully customizable emails and sms - -### ZITADELSecretSettings - -Replaces secret generators - -## API Release Policy - -- Defined but not yet implemented APIs are subject to change without further notice. -- Once an API definition is implemented, it is released as _Preview_ and is available for testing. -- When a _Preview_ API is tested enough so the concepts are proven to work, a new _Beta_ API is released. -- When an API is feature-complete and stable enough, a new _GA_ (General Availability) API is released. -- In all stages, changes to already implemented APIs are done in a backwards-compatible way, if possible. -- When we release a new stage for an API, we deprecate the previous stage and keep it available for a smooth transition. - -## Preview APIs - -These APIs are ready for testing and feedback. -Beware, they don't yet follow all the rules defined above. - - diff --git a/docs/docs/concepts/architecture/secrets.md b/docs/docs/concepts/architecture/secrets.md index 4156f77a26..f8f195114b 100644 --- a/docs/docs/concepts/architecture/secrets.md +++ b/docs/docs/concepts/architecture/secrets.md @@ -70,6 +70,7 @@ The following hash algorithms are supported: - bcrypt (Default) - md5: implementation of md5Crypt with salt and password shuffling [^2] - md5plain: md5 digest of a password without salt [^2] +- md5salted: md5 digest of a salted password [^2] - scrypt - pbkdf2 diff --git a/docs/docs/concepts/architecture/software.md b/docs/docs/concepts/architecture/software.md index 07265b6de5..dc6f2b56c7 100644 --- a/docs/docs/concepts/architecture/software.md +++ b/docs/docs/concepts/architecture/software.md @@ -1,51 +1,51 @@ --- -title: ZITADEL's Software Architecture +title: Zitadel's Software Architecture sidebar_label: Software Architecture --- -ZITADEL is built with two essential patterns. Event Sourcing (ES) and Command and Query Responsibility Segregation (CQRS). -Due to the nature of Event Sourcing ZITADEL provides the unique capability to generate a strong audit trail of ALL the things that happen to its resources, without compromising on storage cost or audit trail length. +Zitadel is built with two essential patterns. Event Sourcing (ES) and Command and Query Responsibility Segregation (CQRS). +Due to the nature of Event Sourcing Zitadel provides the unique capability to generate a strong audit trail of ALL the things that happen to its resources, without compromising on storage cost or audit trail length. -The combination of ES and CQRS makes ZITADEL eventual consistent which, from our perspective, is a great benefit in many ways. +The combination of ES and CQRS makes Zitadel eventual consistent which, from our perspective, is a great benefit in many ways. It allows us to build a Source of Records (SOR) which is the one single point of truth for all computed states. The SOR needs to be transaction safe to make sure all operations are in order. You can read more about this in our [ES documentation](../eventstore/overview). -Each ZITADEL binary contains all components necessary to serve traffic +Each Zitadel binary contains all components necessary to serve traffic From serving the API, rendering GUI's, background processing of events and task. -This All in One (AiO) approach makes operating ZITADEL simple. +This All in One (AiO) approach makes operating Zitadel simple. ## The Architecture -ZITADELs software architecture is built around multiple components at different levels. +Zitadels software architecture is built around multiple components at different levels. This chapter should give you an idea of the components as well as the different layers. ![Software Architecture](/img/zitadel_software_architecture.png) ### Service Layer -The service layer includes all components who are potentially exposed to consumers of ZITADEL. +The service layer includes all components who are potentially exposed to consumers of Zitadel. #### HTTP Server The http server is responsible for the following functions: -- serving the management GUI called ZITADEL Console +- serving the management GUI called Zitadel Console - serving the static assets - rendering server side html (login, password-reset, verification, ...) #### API Server -The API layer consist of the multiple APIs provided by ZITADEL. Each serves a dedicated purpose. -All APIs of ZITADEL are always available as gRCP, gRPC-web and REST service. +The API layer consist of the multiple APIs provided by Zitadel. Each serves a dedicated purpose. +All APIs of Zitadel are always available as gRCP, gRPC-web and REST service. The only exception is the [OpenID Connect & OAuth](/apis/openidoauth/endpoints) and [Asset API](/apis/introduction#assets) due their unique nature. -- [OpenID Connect & OAuth](/apis/openidoauth/endpoints) - allows to request authentication and authorization of ZITADEL -- [SAML](/apis/saml/endpoints) - allows to request authentication and authorization of ZITADEL through the SAML standard +- [OpenID Connect & OAuth](/apis/openidoauth/endpoints) - allows to request authentication and authorization of Zitadel +- [SAML](/apis/saml/endpoints) - allows to request authentication and authorization of Zitadel through the SAML standard - [Authentication API](/apis/introduction#authentication) - allow a user to do operation in its own context -- [Management API](/apis/introduction#management) - allows an admin or machine to manage the ZITADEL resources on an organization level -- [Administration API](/apis/introduction#administration) - allows an admin or machine to manage the ZITADEL resources on an instance level -- [System API](/apis/introduction#system) - allows to create and change new ZITADEL instances +- [Management API](/apis/introduction#management) - allows an admin or machine to manage the Zitadel resources on an organization level +- [Administration API](/apis/introduction#administration) - allows an admin or machine to manage the Zitadel resources on an instance level +- [System API](/apis/introduction#system) - allows to create and change new Zitadel instances - [Asset API](/apis/introduction#assets) - is used to upload and download static assets ### Core Layer @@ -61,7 +61,7 @@ The Command Side has some unique requirements, these include: ##### Command Handler -The command handler receives all operations who alter a resource managed by ZITADEL. +The command handler receives all operations who alter a resource managed by Zitadel. For example if a user changes his name. The API Layer will pass the instruction received through the API call to the command handler for further processing. The command handler is then responsible of creating the necessary commands. After creating the commands the command hand them down to the command validation. @@ -75,14 +75,14 @@ These events now are being handed down to the storage layer for storage. #### Events -ZITADEL handles events in two ways. +Zitadel handles events in two ways. Events that should be processed in near real time are processed by a in memory pub sub system. Some events can be handled asynchronously using the spooler. ##### Pub Sub The pub sub system job is it to keep a query view up-to-date by feeding a constant stream of events to the projections. -Our pub sub system built into ZITADEL works by placing events into an in memory queue for its subscribers. +Our pub sub system built into Zitadel works by placing events into an in memory queue for its subscribers. There is no need for specific guarantees from the pub sub system. Since the SOR is the ES everything can be retried without loss of data. In case of an error an event can be reapplied in two ways: @@ -90,8 +90,8 @@ In case of an error an event can be reapplied in two ways: - The spooler takes care of background cleanups in a scheduled fashion > The decision to incorporate an internal pub sub system with no need for specific guarantees is a deliberate choice. -> We believe that the toll of operating an additional external service like a MQ system negatively affects the ease of use of ZITADEL as well as its availability guarantees. -> One of the authors of ZITADEL did his thesis to test this approach against established MQ systems. +> We believe that the toll of operating an additional external service like a MQ system negatively affects the ease of use of Zitadel as well as its availability guarantees. +> One of the authors of Zitadel did his thesis to test this approach against established MQ systems. ##### Spooler @@ -136,12 +136,16 @@ It is also responsible to execute authorization checks. To check if a request is ### Storage Layer -As ZITADEL itself is built completely stateless only the storage layer is needed to persist states. -The storage layer of ZITADEL is responsible for multiple tasks. For example: +As Zitadel itself is built completely stateless only the storage layer is needed to persist states. +The storage layer of Zitadel is responsible for multiple tasks. For example: - Guarantee strong consistency for the command side - Guarantee good query performance for the query side - Backup and restore operation for disaster recovery purpose -ZITADEL currently supports PostgreSQL and CockroachDB.. +Zitadel currently supports PostgreSQL. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide on using one of them. + +:::info +Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](cli/mirror) to migrate to PostgreSQL. +::: \ No newline at end of file diff --git a/docs/docs/concepts/architecture/solution.md b/docs/docs/concepts/architecture/solution.md index b99b8aa9dc..49e9e8f62f 100644 --- a/docs/docs/concepts/architecture/solution.md +++ b/docs/docs/concepts/architecture/solution.md @@ -1,21 +1,20 @@ --- -title: ZITADEL's Deployment Architecture +title: Zitadel's Deployment Architecture sidebar_label: Deployment Architecture --- ## High Availability -ZITADEL can be run as high available system with ease. +Zitadel can be run as high available system with ease. Since the storage layer takes the heavy lifting of making sure that data in synched across, server, data centers or regions. -Depending on your projects needs our general recommendation is to run ZITADEL and ZITADELs storage layer across multiple availability zones in the same region or if you need higher guarantees run the storage layer across multiple regions. -Consult the [CockroachDB documentation](https://www.cockroachlabs.com/docs/) for more details or use the [CockroachCloud Service](https://www.cockroachlabs.com/docs/cockroachcloud/create-an-account.html) -Alternatively you can run ZITADEL also with Postgres which is [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported). -Make sure to read our [Production Guide](/self-hosting/manage/production#prefer-postgresql) before you decide to use it. +Depending on your projects needs our general recommendation is to run Zitadel across multiple availability zones in the same region or across multiple regions. +Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use it. +Consult the [Postgres documentation](https://www.postgresql.org/docs/) for more details. ## Scalability -ZITADEL can be scaled in a linear fashion in multiple dimensions. +Zitadel can be scaled in a linear fashion in multiple dimensions. - Vertical on your compute infrastructure - Horizontal in a region @@ -23,45 +22,38 @@ ZITADEL can be scaled in a linear fashion in multiple dimensions. Our customers can reuse the same already known binary or container and scale it across multiple server, data center and regions. To distribute traffic an already existing proxy infrastructure can be reused. -Simply steer traffic by path, hostname, IP address or any other metadata to the ZITADEL of your choice. +Simply steer traffic by path, hostname, IP address or any other metadata to the Zitadel of your choice. -> To improve your service quality we recommend steering traffic by path to different ZITADEL deployments +> To improve your service quality we recommend steering traffic by path to different Zitadel deployments > Feel free to [contact us](https://zitadel.com/contact/) for details ## Example Deployment Architecture ### Single Cluster / Region -A ZITADEL Cluster is a highly available IAM system with each component critical for serving traffic laid out at least three times. -As our storage layer (CockroachDB) relies on Raft, it is recommended to operate odd numbers of storage nodes to prevent "split brain" problems. -Hence our reference design for Kubernetes is to have three application nodes and three storage nodes. +A Zitadel Cluster is a highly available IAM system with each component critical for serving traffic laid out at least three times. +Our storage layer (Postgres) is built for single region deployments. +Hence our reference design for Kubernetes is to have three application nodes and one storage node. -> If you are using a serverless offering like Google Cloud Run you can scale ZITADEL from 0 to 1000 Pods without the need of deploying the node across multiple availability zones. - -:::info -CockroachDB needs to be configured with locality flags to proper distribute data over the zones -::: +> If you are using a serverless offering like Google Cloud Run you can scale Zitadel from 0 to 1000 Pods without the need of deploying the node across multiple availability zones. ![Cluster Architecture](/img/zitadel_cluster_architecture.png) ### Multi Cluster / Region -To scale ZITADEL across regions it is recommend to create at least three cluster. -We recommend to run an odd number of storage clusters (storage nodes per data center) to compensate for "split brain" scenarios. -In our reference design we recommend to create one cluster per region or cloud provider with a minimum of three regions. +To scale Zitadel across regions it is recommend to create at least three clusters. +Each cluster is a fully independent ZITADEL setup. +To keep the data in sync across all clusters, we recommend using Postgres with read-only replicas as a storage layer. +Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use it. +Consult the [Postgres documentation](https://www.postgresql.org/docs/current/high-availability.html) for more details. -With this design even the outage of a whole data-center would have a minimal impact as all data is still available at the other two locations. - -:::info -CockroachDB needs to be configured with locality flags to proper distribute data over the zones -::: ![Multi-Cluster Architecture](/img/zitadel_multicluster_architecture.png) ## Zero Downtime Updates Since an Identity system tends to be a critical piece of infrastructure, the "in place zero downtime update" is a well needed feature. -ZITADEL is built in a way that upgrades can be executed without downtime by just updating to a more recent version. +Zitadel is built in a way that upgrades can be executed without downtime by just updating to a more recent version. The common update involves the following steps and do not need manual intervention of the operator: @@ -78,5 +70,5 @@ Users who use [Kubernetes/Helm](/docs/self-hosting/deploy/kubernetes) or serverl :::info As a good practice we recommend creating Database Backups prior to an update. It is also recommend to read the release notes on GitHub before upgrading. -Since ZITADEL utilizes Semantic Versioning Breaking Changes of any kind will always increase the major version (e.g Version 2 would become Version 3). +Since Zitadel utilizes Semantic Versioning Breaking Changes of any kind will always increase the major version (e.g Version 2 would become Version 3). ::: diff --git a/docs/docs/concepts/features/actions_v2.md b/docs/docs/concepts/features/actions_v2.md index a06384639d..1617081222 100644 --- a/docs/docs/concepts/features/actions_v2.md +++ b/docs/docs/concepts/features/actions_v2.md @@ -10,6 +10,12 @@ This is useful when you have special business requirements that ZITADEL doesn't We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features. ::: +:::warning +To use Actions v2 activate the feature flag "Actions" [feature flag](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features), to be able to manage the related resources. + +The Actions v2 will always be executed if available, even if the feature flag is switched off, to remove any Actions v2 the related Execution has to be removed. +::: + ## Why actions? ZITADEL can't anticipate and solve every possible business rule and integration requirements from all ZITADEL users. Here are some examples: - A business requires domain specific data validation before a user can be created or authenticated. @@ -31,10 +37,25 @@ so that everybody can implement their custom behaviour for as many processes as Possible conditions for the Execution: - Request, to react to or manipulate requests to ZITADEL, for example add information to newly created users - Response, to react to or manipulate responses to ZITADEL, for example to provision newly created users to other systems -- Function, to react to different functionality in ZITADEL, replaces [Actions](/concepts/features/actions) +- Function, to react to different functionality in ZITADEL, replaces [Actions](/concepts/features/actions). - Event, to create to different events which get created in ZITADEL, for example to inform somebody if a user gets locked +:::info +Currently, the defined Actions v2 will be executed additionally to the defined [Actions](/concepts/features/actions). +::: + +## Migration + +- [Migrate Actions v1 to Actions v2](/guides/integrate/actions/migrate-from-v1) + ## Further reading -- [Actions v2 reference](/apis/actions/v3/usage) -- [Actions v2 example execution locally](/apis/actions/v3/testing-locally) \ No newline at end of file +- [Actions v2 reference](/guides/integrate/actions/usage) +- [Actions v2 example execution for request](/guides/integrate/actions/testing-request) +- [Actions v2 example execution for request manipulation](/guides/integrate/actions/testing-request-manipulation) +- [Actions v2 example execution for request signature check](/guides/integrate/actions/testing-request-signature) +- [Actions v2 example execution for response](/guides/integrate/actions/testing-response) +- [Actions v2 example execution for response manipulation](/guides/integrate/actions/testing-response-manipulation) +- [Actions v2 example execution for function](/guides/integrate/actions/testing-function) +- [Actions v2 example execution for function manipulation](/guides/integrate/actions/testing-function-manipulation) +- [Actions v2 example execution for event](/guides/integrate/actions/testing-event) \ No newline at end of file diff --git a/docs/docs/guides/integrate/actions/migrate-from-v1.md b/docs/docs/guides/integrate/actions/migrate-from-v1.md new file mode 100644 index 0000000000..2ea2b3f1e3 --- /dev/null +++ b/docs/docs/guides/integrate/actions/migrate-from-v1.md @@ -0,0 +1,114 @@ +--- +title: Migrate from Actions v1 to v2 +--- + +In this guide, you will have all necessary information to migrate from Actions v1 to Actions v2 with all currently [available Flow Types](/apis/actions/introduction#available-flow-types). + +## Internal Authentication + +### Post Authentication + +A user has authenticated directly at ZITADEL. +ZITADEL validated the users inputs for password, one-time password, security key or passwordless factor. + +To react to different authentication actions, the session service, `zitadel.session.v2.SessionService`, provides the different endpoints. As a rule of thumb, use response triggers if you primarily want to handle successful and failed authentications. On the other hand, use event triggers if you need more fine-granular handling, for example by the used authentication factors. + +Some use-cases: + +- Handle successful authentication through the response of `/zitadel.session.v2.SessionService/CreateSession` and `/zitadel.session.v2.SessionService/SetSession`, [Action Response Example](./testing-response) +- Handle failed authentication through the response of `/zitadel.session.v2.SessionService/CreateSession` and `/zitadel.session.v2.SessionService/SetSession`, [Action Response Example](./testing-response) +- Handle session with password checked through the creation of event `session.password.checked`, [Action Event Example](./testing-event) +- Handle successful authentication through the creation of event `user.human.password.check.succeeded`, [Action Event Example](./testing-event) +- Handle failed authentication through the creation of event `user.human.password.check.failed`, [Action Event Example](./testing-event) + +### Pre Creation + +A user registers directly at ZITADEL. +ZITADEL did not create the user yet. + +Some use-cases: + +- Before a user is created through the request on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Request Example](./testing-request) +- Add information to the user through the request on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Request Manipulation Example](./testing-request-manipulation) + +### Post Creation + +A user registers directly at ZITADEL. +ZITADEL successfully created the user. + +Some use-cases: + +- After user is created through the response on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Response Example](./testing-response) +- At the event of a user creation on `user.human.added`, [Action Event Example](./testing-event) + +## External Authentication + +### Post Authentication + +A user has authenticated externally. ZITADEL retrieved and mapped the external information. + +Some use-cases: + +- Handle the information mapping from the external authentication to internal structure through the response on `/zitadel.user.v2.UserService/RetrieveIdentityProviderIntent`, [Action Response Example](./testing-response) + - information about the link to the external IDP available in the response under [`idpInformation`](/apis/resources/user_service_v2/user-service-retrieve-identity-provider-intent) + - information if a new user has to be created available in the response under [`addHumanUser`](/apis/resources/user_service_v2/user-service-retrieve-identity-provider-intent), including metadata and link to external IDP + +### Pre Creation + +A user registers directly at ZITADEL. +ZITADEL did not create the user yet. + +Some use-cases: + +- Before a user is created through the request on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Request Example](./testing-request) +- Add information to the user through the request on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Request Manipulation Example](./testing-request-manipulation) + +### Post Creation + +A user registers directly at ZITADEL. +ZITADEL successfully created the user. + +Some use-cases: + +- After user is created through the response on `/zitadel.user.v2.UserService/AddHumanUser`, [Action Response Example](./testing-response) +- At the event of a user creation on `user.human.added`, [Action Event Example](./testing-event) + +## Complement Token + +These are executed during the creation of tokens and token introspection. + +### Pre Userinfo + +These are called before userinfo are set in the id_token or userinfo and introspection endpoint response. + +Some use-cases: + +- Add claims to the userinfo through function on `preuserinfo`, [Action Function Example](./testing-function) +- Add metadata to user through function on `preuserinfo`, [Action Function Example](./testing-function) +- Add logs to the log claim through function on `preuserinfo`, [Action Function Example](./testing-function) + +### Pre Access Token + +These are called before the claims are set in the access token and the token type is `jwt`. + +Some use-cases: + +- Add claims to the userinfo through function on `preaccesstoken`, [Action Function Example](./testing-function) +- Add metadata to user through function on `preaccesstoken`, [Action Function Example](./testing-function) +- Add logs to the log claim through function on `preaccesstoken`, [Action Function Example](./testing-function) + +## Customize SAML Response + +These are executed before the return of the SAML Response. + +### Pre SAMLResponse Creation + +These are called before attributes are set in the SAMLResponse. + +Some use-cases: + +- Add custom attributes to the response through function on `presamlresponse`, [Action Function Example](./testing-function) +- Add metadata to user through function on `presamlresponse`, [Action Function Example](./testing-function) + + + diff --git a/docs/docs/guides/integrate/actions/testing-event.md b/docs/docs/guides/integrate/actions/testing-event.md new file mode 100644 index 0000000000..8b4502703b --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-event.md @@ -0,0 +1,176 @@ +--- +title: Test Actions Event +--- + +This guide shows you how to leverage the ZITADEL actions feature to react to events in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific event occurs. +This is useful for integrating with other systems or for triggering workflows based on events in ZITADEL. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an event occurs. +You will need to implement a listener that can receive HTTP requests and process the events. +For this example, we will use a simple Go HTTP server that will print the received events to standard output. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the +target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local webhook", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure ZITADEL to call the target when an event occurs, you need to set an execution and define the event +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "event": { + "event": "user.human.added" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "userId": { + "givenName": "Test", + "familyName": "User" + }, + "email": { + "email": "example@test.com" + } +}' +``` + +Your server should now print out something like the following. Check out +the [Sent information Event](./usage#sent-information-event) payload description. + +```json +{ + "aggregateID": "313014806065971608", + "aggregateType": "user", + "resourceOwner": "312909075211944344", + "instanceID": "312909075211878808", + "version": "v2", + "sequence": 1, + "event_type": "user.human.added", + "created_at": "2025-03-27T10:22:43.262665+01:00", + "userID": "312909075212468632", + "event_payload": { + "userName":"example@test.com", + "firstName":"Test", + "lastName":"User", + "displayName":"Test User", + "preferredLanguage":"und", + "email":"example@test.com" + } +} +``` + +The event_payload is base64 encoded and has the following content: + +```json +{ + "userName": "example@test.com", + "firstName": "Test", + "lastName": "User", + "displayName": "Test User", + "preferredLanguage": "und", + "email": "example@test.com" +} +``` + +## Conclusion + +You have successfully set up a target and execution to react to events in your ZITADEL instance. +This feature can now be used to integrate with your existing systems to create custom workflows or automate tasks based on events in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). \ No newline at end of file diff --git a/docs/docs/guides/integrate/actions/testing-function-manipulation.md b/docs/docs/guides/integrate/actions/testing-function-manipulation.md new file mode 100644 index 0000000000..8f5e2fc968 --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-function-manipulation.md @@ -0,0 +1,155 @@ +--- +title: Test Actions Function Manipulation +--- + +This guide shows you how to leverage the ZITADEL actions feature to enhance different functions in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific functionality is used. +This is useful for integrating with other systems which need specific claims in tokens or for executing external code during OIDC or SAML flows. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +## Available functions + +The available conditions can be found under [all available Functions](/apis/resources/action_service_v2/action-service-list-execution-functions). + +## Start example target + +To test the actions feature, you need to create a target that will be called when a function is used. +You will need to implement a listener that can receive HTTP requests and process the data. +For this example, we will use a simple Go HTTP server that will send back static data. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "encoding/json" + "net/http" +) + +type Response struct { + SetUserMetadata []*Metadata `json:"set_user_metadata,omitempty"` + AppendClaims []*AppendClaim `json:"append_claims,omitempty"` + AppendLogClaims []string `json:"append_log_claims,omitempty"` +} + +type Metadata struct { + Key string `json:"key"` + Value []byte `json:"value"` +} + +type AppendClaim struct { + Key string `json:"key"` + Value any `json:"value"` +} + +// call HandleFunc to respond with static data +func call(w http.ResponseWriter, req *http.Request) { + // create the response with the correct structure + resp := &Response{ + SetUserMetadata: []*Metadata{ + {Key: "key", Value: []byte("value")}, + }, + AppendClaims: []*AppendClaim{ + {Key: "claim", Value: "value"}, + }, + AppendLogClaims: []string{"log1", "log2", "log3"}, + } + data, err := json.Marshal(resp) + if err != nil { + // if there was an error while marshalling the json + http.Error(w, "error", http.StatusInternalServerError) + return + } + w.Write(data) +} + +func main() { + // handle the HTTP call under "/call" + http.HandleFunc("/call", call) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} + +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local call", + "restCall": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/call", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure ZITADEL to call the target when a function is executed, you need to set an execution and define the function +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "function": { + "name": "preuserinfo" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by logging into Console UI or +by using any OIDC flow. + +As a result 3 things happen: +- the user get the metadata with the key "key" and value "value" added +- the token has a claim "urn:zitadel:iam:claim" added with value "value" +- the token has the log claim "urn:zitadel:iam:action:preuserinfo:log" added with values "log1", "log2" and "log3". + +For any further information related to [the OIDC Flow, refer to our documentation.](/guides/integrate/login/oidc/login-users) + +## Conclusion + +You have successfully set up a target and execution to react to functions in your ZITADEL instance. +This feature can now be used to integrate with your existing systems to create custom workflows or automate tasks based on functionality in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-function.md b/docs/docs/guides/integrate/actions/testing-function.md new file mode 100644 index 0000000000..f14f20b69d --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-function.md @@ -0,0 +1,171 @@ +--- +title: Test Actions Function +--- + +This guide shows you how to leverage the ZITADEL actions feature to enhance different functions in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific functionality is used. +This is useful for integrating with other systems which need specific claims in tokens or for executing external code during OIDC or SAML flows. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +## Available functions + +The available conditions can be found under [all available Functions](/apis/resources/action_service_v2/action-service-list-execution-functions). + +## Start example target + +To test the actions feature, you need to create a target that will be called when a function is used. +You will need to implement a listener that can receive HTTP requests and process the data. +For this example, we will use a simple Go HTTP server that will print the received data to standard output. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} + +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local call", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure ZITADEL to call the target when a function is executed, you need to set an execution and define the function +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "function": { + "name": "preuserinfo" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by logging into Console UI or +by using any OIDC flow. + +Your server should now print out something like the following. Check out the [Sent information Function](./usage#sent-information-function) payload description. +```json +{ + "function" : "function/preuserinfo", + "userinfo" : { + "sub" : "312909075212468632" + }, + "user" : { + "id" : "312909075212468632", + "creation_date" : "2025-03-26T15:52:23.917636Z", + "change_date" : "2025-03-26T15:52:23.917636Z", + "resource_owner" : "312909075211944344", + "sequence" : 2, + "state" : 1, + "username" : "user@example.com", + "preferred_login_name" : "zitadel@zitadel.localhost", + "human" : { + "first_name" : "Example firstname", + "last_name" : "Example lastname", + "display_name" : "Example displayname", + "preferred_language" : "en", + "email" : "user@example.com", + "is_email_verified" : true, + "password_changed" : "0001-01-01T00:00:00Z", + "mfa_init_skipped" : "0001-01-01T00:00:00Z" + } + }, + "user_metadata" : [ { + "creation_date" : "2025-03-27T09:10:25.879677Z", + "change_date" : "2025-03-27T09:10:25.879677Z", + "resource_owner" : "312909075211944344", + "sequence" : 18, + "key" : "key", + "value" : "dmFsdWU=" + } ], + "org" : { + "id" : "312909075211944344", + "name" : "ZITADEL", + "primary_domain" : "example.com" + } +} +``` + +For any further information related to [the OIDC Flow, refer to our documentation.](/guides/integrate/login/oidc/login-users) + +## Conclusion + +You have successfully set up a target and execution to react to functions in your ZITADEL instance. +This feature can now be used to customize the functionality in ZITADEL, in particular the content of the OIDC tokens and SAML responses. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-request-manipulation.md b/docs/docs/guides/integrate/actions/testing-request-manipulation.md new file mode 100644 index 0000000000..1cb4f1776a --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-request-manipulation.md @@ -0,0 +1,211 @@ +--- +title: Test Actions Request Manipulation +--- + +This guide shows you how to leverage the ZITADEL actions feature to manipulate API requests in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API request occurs. +This is useful for adding information to managed resources in ZITADEL. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests, process the request and returns the manipulated request. +For this example, we will use a simple Go HTTP server that will return the request with added metadata. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "google.golang.org/protobuf/encoding/protojson" +) + +type contextRequest struct { + Request *addHumanUserRequestWrapper `json:"request"` +} + +// addHumanUserRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type addHumanUserRequestWrapper struct { + user.AddHumanUserRequest +} + +func (r *addHumanUserRequestWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *addHumanUserRequestWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) +} + +// call HandleFunc to read the request body, manipulate the content and return the manipulated request +func call(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + + // read the request into the expected structure + request := new(contextRequest) + if err := json.Unmarshal(sentBody, request); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + } + + // build the response from the received request + response := request.Request + // manipulate the request to send back as response + if response.Metadata == nil { + response.Metadata = make([]*user.SetMetadataEntry, 0) + } + response.Metadata = append(response.Metadata, &user.SetMetadataEntry{Key: "organization", Value: []byte("company")}) + + // marshal the request into json + data, err := json.Marshal(response) + if err != nil { + // if there was an error while marshalling the json + http.Error(w, "error", http.StatusInternalServerError) + return + } + + // return the manipulated request + w.Write(data) +} + +func main() { + // handle the HTTP call under "/call" + http.HandleFunc("/call", call) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} + +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local call", + "restCall": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/call", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To call the target just created before, with the intention to manipulate the request used for user creation by the user V2 API, we define an execution with a method condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "request": { + "method": "/zitadel.user.v2.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "profile": { + "givenName": "Example_given", + "familyName": "Example_family" + }, + "email": { + "email": "example@example.com" + } +}' +``` + +Your server should now manipulate the request to something like the following. Check out +the [Sent information Request](./usage#sent-information-request) payload description. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "profile": { + "givenName": "Example_given", + "familyName": "Example_family" + }, + "email": { + "email": "example@example.com" + } + "metadata": [ + {"key": "organization", "value": "Y29tcGFueQ=="} + ] +}' +``` + +## Conclusion + +You have successfully set up a target and execution to manipulate API requests in your ZITADEL instance. +This feature can now be used to add or manipulate information to managed resources in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/apis/actions/v3/testing-locally.md b/docs/docs/guides/integrate/actions/testing-request-signature.md similarity index 51% rename from docs/docs/apis/actions/v3/testing-locally.md rename to docs/docs/guides/integrate/actions/testing-request-signature.md index b5b3cb389f..c1932a7d5b 100644 --- a/docs/docs/apis/actions/v3/testing-locally.md +++ b/docs/docs/guides/integrate/actions/testing-request-signature.md @@ -1,8 +1,10 @@ --- -title: Test Actions Locally +title: Test Actions Request Signature Check --- -In this guide, you will create a ZITADEL execution and target. After a user is created through the API, the target is called. +This guide shows you how to leverage the ZITADEL actions feature to react to API requests in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API request occurs. +This is useful for information provisioning in between systems or for triggering workflows based on API requests in ZITADEL. ## Prerequisites @@ -11,9 +13,22 @@ Before you start, make sure you have everything set up correctly. - You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) - Your ZITADEL instance needs to have the actions feature enabled. +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + ## Start example target -To start a simple HTTP server locally, which receives the webhook call, the following code example can be used: +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests, check the signature and process the request. +For this example, we will use a simple Go HTTP server that will print the received request to standard output. +The 'signingKey' is the key received in the next step 'Create target'. ```go package main @@ -22,8 +37,12 @@ import ( "fmt" "io" "net/http" + + "github.com/zitadel/zitadel/pkg/actions" ) +const signingKey = "somekey" + // webhook HandleFunc to read the request body and then print out the contents func webhook(w http.ResponseWriter, req *http.Request) { // read the body content @@ -33,6 +52,13 @@ func webhook(w http.ResponseWriter, req *http.Request) { http.Error(w, "error", http.StatusInternalServerError) return } + defer req.Body.Close() + // validate signature + if err := actions.ValidatePayload(sentBody, req.Header.Get(actions.SigningHeader), signingKey); err != nil { + // if the signed content is not equal the sent content return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } // print out the read content fmt.Println(string(sentBody)) } @@ -46,30 +72,14 @@ func main() { } ``` -What happens here is only a target which prints out the received request, which could also be handled with a different logic. - -### Check Signature - -To additionally check the signature header you can add the following to the example: -```go - // validate signature - if err := actions.ValidatePayload(sentBody, req.Header.Get(actions.SigningHeader), signingKey); err != nil { - // if the signed content is not equal the sent content return an error - http.Error(w, "error", http.StatusInternalServerError) - return - } -``` - -Where you can replace 'signingKey' with the key received in the next step 'Create target'. - ## Create target As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: -[Create a target](/apis/resources/action_service_v3/zitadel-actions-create-target) +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. ```shell -curl -L -X POST 'https://$CUSTOM-DOMAIN/v3alpha/targets' \ +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -87,12 +97,13 @@ Save the returned ID to set in the execution. ## Set execution -To call the target just created before, with the intention to print the request used for user creation by the user V2 API, we define an execution with a method condition. +To configure ZITADEL to call the target when an API endpoint is called, you need to set an execution and define the request +condition. -[Set an execution](/apis/resources/action_service_v3/zitadel-actions-set-execution) +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. ```shell -curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \ +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -112,7 +123,8 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \ ## Example call -Now on every call on `/zitadel.user.v2.UserService/AddHumanUser` the local server prints out the received body of the request: +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. ```shell curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ @@ -130,7 +142,9 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ }' ``` -Should print out something like, also described under [Sent information Request](./usage#sent-information-request): +Your server should now print out something like the following. Check out +the [Sent information Request](./usage#sent-information-request) payload description. + ```shell { "fullMethod": "/zitadel.user.v2.UserService/AddHumanUser", @@ -150,4 +164,9 @@ Should print out something like, also described under [Sent information Request] } ``` +## Conclusion +You have successfully set up a target and execution to react to API requests in your ZITADEL instance. +This feature can now be used to provision information in between systems or for triggering workflows based on API requests in ZITADEL. +Additionally, you are sure that the request was not tempered with, as the signature was created with the combination of signing key and payload. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-request.md b/docs/docs/guides/integrate/actions/testing-request.md new file mode 100644 index 0000000000..b2413e606e --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-request.md @@ -0,0 +1,164 @@ +--- +title: Test Actions Request +--- + +This guide shows you how to leverage the ZITADEL actions feature to react to API requests in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API request occurs. +This is useful for information provisioning in between systems or for triggering workflows based on API requests in ZITADEL. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests and process the request. +For this example, we will use a simple Go HTTP server that will print the received request to standard output. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local webhook", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure ZITADEL to call the target when an API endpoint is called, you need to set an execution and define the request +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "request": { + "method": "/zitadel.user.v2.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "profile": { + "givenName": "Test", + "familyName": "User" + }, + "email": { + "email": "example@test.com" + } +}' +``` + +Your server should now print out something like the following. Check out +the [Sent information Request](./usage#sent-information-request) payload description. + +```shell +{ + "fullMethod": "/zitadel.user.v2.UserService/AddHumanUser", + "instanceID": "262851882718855632", + "orgID": "262851882718921168", + "projectID": "262851882719052240", + "userID": "262851882718986704", + "request": { + "profile": { + "given_name": "Test", + "family_name": "User" + }, + "email": { + "email": "example@test.com" + } + } +} +``` + +## Conclusion + +You have successfully set up a target and execution to react to API requests in your ZITADEL instance. +This feature can now be used to provision information in between systems or for triggering workflows based on API requests in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-response-manipulation.md b/docs/docs/guides/integrate/actions/testing-response-manipulation.md new file mode 100644 index 0000000000..9d95479b05 --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-response-manipulation.md @@ -0,0 +1,294 @@ +--- +title: Test Actions Response Manipulation +--- + +This guide shows you how to leverage the ZITADEL actions feature to manipulate API responses in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API response occurs. +This is useful for triggering workflows based on API responses in ZITADEL. You can even use this to provide data necessary data to the new login UI as shown in this example. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests, process the request and returns the manipulated request. +For this example, we will use a simple Go HTTP server that will return the request with added metadata. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "google.golang.org/protobuf/encoding/protojson" +) + +type contextResponse struct { + Request *retrieveIdentityProviderIntentRequestWrapper `json:"request"` + Response *retrieveIdentityProviderIntentResponseWrapper `json:"response"` +} + +// RetrieveIdentityProviderIntentRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type retrieveIdentityProviderIntentRequestWrapper struct { + user.RetrieveIdentityProviderIntentRequest +} + +func (r *retrieveIdentityProviderIntentRequestWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *retrieveIdentityProviderIntentRequestWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) +} + +// RetrieveIdentityProviderIntentResponseWrapper necessary to marshal and unmarshal the JSON into the proto message correctly +type retrieveIdentityProviderIntentResponseWrapper struct { + user.RetrieveIdentityProviderIntentResponse +} + +func (r *retrieveIdentityProviderIntentResponseWrapper) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *retrieveIdentityProviderIntentResponseWrapper) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r) +} + +// call HandleFunc to read the response body, manipulate the content and return the response +func call(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + + // read the response into the expected structure + request := new(contextResponse) + if err := json.Unmarshal(sentBody, request); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + } + + // build the response from the received response + resp := request.Response + // manipulate the received response to send back as response + if resp != nil && resp.AddHumanUser != nil { + // manipulate the response + resp.AddHumanUser.Metadata = append(resp.AddHumanUser.Metadata, &user.SetMetadataEntry{Key: "organization", Value: []byte("company")}) + } + + // marshal the response into json + data, err := json.Marshal(resp) + if err != nil { + // if there was an error while marshalling the json + http.Error(w, "error", http.StatusInternalServerError) + return + } + + // return the manipulated response + w.Write(data) +} + +func main() { + // handle the HTTP call under "/call" + http.HandleFunc("/call", call) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the +target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local call", + "restCall": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/call", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To call the target just created before, with the intention to manipulate the retrieve of an intent by the user V2 API, +we define an execution with a response condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "response": { + "method": "/zitadel.user.v2.UserService/RetrieveIdentityProviderIntent" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by using a login-flow in the typescript login with an external IDP. + +```json +{ + "details": { + "sequence": "599", + "changeDate": "2023-06-15T06:44:26.039444Z", + "resourceOwner": "163840776835432705" + }, + "idpInformation": { + "oauth": { + "accessToken": "ya29...", + "idToken": "ey..." + }, + "idpId": "218528353504723201", + "userId": "218528353504723202", + "username": "test-user@localhost", + "rawInformation": { + "User": { + "email": "test-user@localhost", + "email_verified": true, + "family_name": "User", + "given_name": "Test", + "hd": "mouse.com", + "locale": "de", + "name": "Minnie Mouse", + "picture": "https://lh3.googleusercontent.com/a/AAcKTtf973Q7NH8KzKTMEZELPU9lx45WpQ9FRBuxFdPb=s96-c", + "sub": "111392805975715856637" + } + } + }, + "addHumanUser": { + "idpLinks": [ + {"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"} + ], + "username": "test-user@localhost", + "profile": { + "givenName": "Test", + "familyName": "User", + "displayName": "Test User", + "preferredLanguage": "de" + }, + "email": { + "email": "test-user@zitadel.ch", + "isVerified": true + }, + "metadata": [] + } +} +``` + +Your server should now manipulate the response to something like the following. Check out +the [Sent information Response](./usage#sent-information-response) payload description. + +```json +{ + "details": { + "sequence": "599", + "changeDate": "2023-06-15T06:44:26.039444Z", + "resourceOwner": "163840776835432705" + }, + "idpInformation": { + "oauth": { + "accessToken": "ya29...", + "idToken": "ey..." + }, + "idpId": "218528353504723201", + "userId": "218528353504723202", + "username": "test-user@localhost", + "rawInformation": { + "User": { + "email": "test-user@localhost", + "email_verified": true, + "family_name": "User", + "given_name": "Test", + "hd": "mouse.com", + "locale": "de", + "name": "Minnie Mouse", + "picture": "https://lh3.googleusercontent.com/a/AAcKTtf973Q7NH8KzKTMEZELPU9lx45WpQ9FRBuxFdPb=s96-c", + "sub": "111392805975715856637" + } + } + }, + "addHumanUser": { + "idpLinks": [ + {"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"} + ], + "username": "test-user@localhost", + "profile": { + "givenName": "Test", + "familyName": "User", + "displayName": "Test User", + "preferredLanguage": "de" + }, + "email": { + "email": "test-user@zitadel.ch", + "isVerified": true + }, + "metadata": [ + {"key": "organization", "value": "Y29tcGFueQ=="} + ] + } +} +``` + +## Conclusion + +You have successfully set up a target and execution to manipulate API responses in your ZITADEL instance. +This feature can now be used to add necessary information for clients including the new login UI. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/testing-response.md b/docs/docs/guides/integrate/actions/testing-response.md new file mode 100644 index 0000000000..a2ab736505 --- /dev/null +++ b/docs/docs/guides/integrate/actions/testing-response.md @@ -0,0 +1,172 @@ +--- +title: Test Actions Response +--- + +This guide shows you how to leverage the ZITADEL actions feature to react to API responses in your ZITADEL instance. +You can use the actions feature to create a target that will be called when a specific API response occurs. +This is useful for information provisioning in between systems or for triggering workflows based on API responses in ZITADEL. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +:::info +Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`. +In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL. +::: + +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + +## Start example target + +To test the actions feature, you need to create a target that will be called when an API endpoint is called. +You will need to implement a listener that can receive HTTP requests and process the request. +For this example, we will use a simple Go HTTP server that will print the received request to standard output. + +:::info +The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature). +::: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + defer req.Body.Close() + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: + +See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information. + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/actions/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local webhook", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To configure Zitadel to call the target when an API endpoint is called, you need to set an execution and define the response +condition. + +See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "response": { + "method": "/zitadel.user.v2.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now that you have set up the target and execution, you can test it by creating a user through the Console UI or +by calling the ZITADEL API to create a human user. + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "userId": { + "givenName": "Example_given", + "familyName": "Example_family" + }, + "email": { + "email": "example@example.com" + } +}' +``` + +Your server should now print out something like the following. Check out +the [Sent information Response](./usage#sent-information-response) payload description. + +```json +{ + "fullMethod": "/zitadel.user.v2.UserService/AddHumanUser", + "instanceID": "262851882718855632", + "orgID": "262851882718921168", + "projectID": "262851882719052240", + "userID": "262851882718986704", + "request": { + "profile": { + "given_name": "Example_given", + "family_name": "Example_family" + }, + "email": { + "email": "example@example.com" + } + }, + "response": { + "user_id": "312918757460672920", + "details": { + "sequence": "2", + "change_date": "2025-03-26T17:28:33.856436Z", + "resource_owner": "312909075211944344", + } + } +} +``` + +## Conclusion + +You have successfully set up a target and execution to react to API responses in your ZITADEL instance. +This feature can now be used to provision information in between systems or for triggering workflows based on API responses in ZITADEL. +Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2). diff --git a/docs/docs/guides/integrate/actions/usage.md b/docs/docs/guides/integrate/actions/usage.md new file mode 100644 index 0000000000..ba512ae549 --- /dev/null +++ b/docs/docs/guides/integrate/actions/usage.md @@ -0,0 +1,502 @@ +--- +title: Using Actions +--- + +The Action API provides a flexible mechanism for customizing and extending the functionality of ZITADEL. By allowing you to define targets and executions, you can implement custom workflows triggered on an API requests and responses, events or specific functions. + +**How it works:** +- Create Target +- Set Execution with condition and target +- Custom Code will be triggered and executed + +**Use Cases:** +- User Management: Automate provisioning user data to external systems when users are crreated, updated or deleted. +- Security: Implement IP blocking or rate limiting based on API usage patterns. +- Extend Workflows: Automatically setup resources in your application, when a new organization in ZITADEL is created. +- Token extension: Add custom claims to the tokens. + +## Endpoints + +ZITADEL sends an HTTP Post request to the endpoint set as Target, the received request than can be edited and send back or custom processes can be handled. + +### Sent information Request + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "fullMethod": "full method of the GRPC call", + "instanceID": "instanceID of the called instance", + "orgID": "ID of the organization related to the calling context", + "projectID": "ID of the project related to the used application", + "userID": "ID of the calling user", + "request": { + "attribute": "Attribute value of full request of the call" + } +} +``` + +:::warning +To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request is a protocol buffer message, to avoid potential problems with the attribute names. +::: + +### Sent information Response + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "fullMethod": "full method of the GRPC call", + "instanceID": "instanceID of the called instance", + "orgID": "ID of the organization related to the calling context", + "projectID": "ID of the project related to the used application", + "userID": "ID of the calling user", + "request": { + "attribute": "Attribute value of full request of the call" + }, + "response": { + "attribute": "Attribute value of full response of the call" + } +} +``` + +:::warning +To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), +as the request and response are protocol buffer messages, to avoid potential problems with the attribute names. +::: + +### Sent information Function + +Information sent and expected back are specific to the function. + +#### PreUserinfo + +The information sent to the Endpoint is structured as JSON: +```json +{ + "function": "Name of the function", + "userinfo": { + "given_name": "", + "family_name": "", + "middle_name": "", + "nickname": "", + "profile": "", + "picture": "", + ... + "preferred_username": "", + "email": "", + "email_verified": true, + "phone_number": "", + "phone_number_verified": true + }, + "user": { + "id": "", + "creation_date": "", + ... + "human": { + "first_name": "", + "last_name": "", + ... + "email": "", + "is_email_verified": true, + "phone": "", + "is_phone_verified": true + } + }, + "user_metadata": [ + { + "creation_date": "", + "change_date": "", + "resource_owner": "", + "sequence": "", + "key": "", + "value": "" + } + ], + "org": { + "id": "ID of the organization the user belongs to", + "name": "Name of the organization the user belongs to", + "primary_domain": "Primary domain of the organization the user belongs to" + }, + "user_grants": [ + { + "id": "", + "projectGrantId": "The ID of the project grant", + "state": 1, + "creationDate": "", + "changeDate": "", + "sequence": 1, + "userId": "", + "roles": [ + "role" + ], + "userResourceOwner": "The ID of the organization the user belongs to", + "userGrantResourceOwner": "The ID of the organization the user got authorization granted", + "userGrantResourceOwnerName": "The name of the organization the user got authorization granted", + "projectId": "", + "projectName": "" + } + ] +} +``` + +The expected structure of the JSON as response: + +```json +{ + "set_user_metadata": [ + { + "key": "key of metadata to be set on the user", + "value": "base64 value of metadata to be set on the user" + } + ], + "append_claims": [ + { + "key": "key of claim to be set on the user", + "value": "value of claim to be set on the user" + } + ], + "append_log_claims": [ + "Log to be appended to the log claim on the token" + ] +} +``` + +#### PreAccessToken + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "function": "Name of the function", + "userinfo": { + "given_name": "", + "family_name": "", + "middle_name": "", + "nickname": "", + "profile": "", + "picture": "", + ... + "preferred_username": "", + "email": "", + "email_verified": true/false, + "phone_number": "", + "phone_number_verified": true/false + }, + "user": { + "id": "", + "creation_date": "", + ... + "human": { + "first_name": "", + "last_name": "", + ... + "email": "", + "is_email_verified": true, + "phone": "", + "is_phone_verified": true + } + }, + "user_metadata": [ + { + "creation_date": "", + "change_date": "", + "resource_owner": "", + "sequence": "", + "key": "", + "value": "" + } + ], + "org": { + "id": "ID of the organization the user belongs to", + "name": "Name of the organization the user belongs to", + "primary_domain": "Primary domain of the organization the user belongs to" + }, + "user_grants": [ + { + "id": "", + "projectGrantId": "The ID of the project grant", + "state": 1, + "creationDate": "", + "changeDate": "", + "sequence": 1, + "userId": "", + "roles": [ + "role" + ], + "userResourceOwner": "The ID of the organization the user belongs to", + "userGrantResourceOwner": "The ID of the organization the user got authorization granted", + "userGrantResourceOwnerName": "The name of the organization the user got authorization granted", + "projectId": "", + "projectName": "" + } + ] +} +``` + +The expected structure of the JSON as response: + +```json +{ + "set_user_metadata": [ + { + "key": "key of metadata to be set on the user", + "value": "base64 value of metadata to be set on the user" + } + ], + "append_claims": [ + { + "key": "key of claim to be set on the user", + "value": "value of claim to be set on the user" + } + ], + "append_log_claims": [ + "Log to be appended to the log claim on the token" + ] +} +``` + +#### PreSAMLResponse + +The information sent to the Endpoint is structured as JSON: +```json +{ + "function": "Name of the function", + "userinfo": { + "given_name": "", + "family_name": "", + "middle_name": "", + "nickname": "", + "profile": "", + "picture": "", + ... + "preferred_username": "", + "email": "", + "email_verified": true, + "phone_number": "", + "phone_number_verified": true + }, + "user": { + "id": "", + "creation_date": "", + ... + "human": { + "first_name": "", + "last_name": "", + ... + "email": "", + "is_email_verified": true, + "phone": "", + "is_phone_verified": true + } + }, + "user_grants": [ + { + "id": "", + "projectGrantId": "The ID of the project grant", + "state": 1, + "creationDate": "", + "changeDate": "", + "sequence": 1, + "userId": "", + "roles": [ + "role" + ], + "userResourceOwner": "The ID of the organization the user belongs to", + "userGrantResourceOwner": "The ID of the organization the user got authorization granted", + "userGrantResourceOwnerName": "The name of the organization the user got authorization granted", + "projectId": "", + "projectName": "" + } + ] +} +``` + +The expected structure of the JSON as response: + +```json +{ + "set_user_metadata": [ + { + "key": "key of metadata to be set on the user", + "value": "base64 value of metadata to be set on the user" + } + ], + "append_attribute": [ + { + "name": "name of the attribute to be added to the response", + "name_format": "name format of the attribute to be added to the response", + "value": "value of the attribute to be added to the response" + } + ] +} +``` + +### Sent information Event + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "aggregateID": "ID of the aggregate", + "aggregateType": "Type of the aggregate", + "resourceOwner": "Resourceowner the aggregate belongs to", + "instanceID": "ID of the instance the aggregate belongs to", + "version": "Version of the aggregate", + "sequence": "Sequence of the event", + "event_type": "Type of the event", + "created_at": "Time the event was created", + "userID": "ID of the creator of the event", + "event_payload": "Content of the event in JSON format" +} +``` + +## Target + +The Target describes how ZITADEL interacts with the Endpoint. + +There are different types of Targets: + +- `Webhook`, the call handles the status code but response is irrelevant, can be InterruptOnError +- `Call`, the call handles the status code and response, can be InterruptOnError +- `Async`, the call handles neither status code nor response, but can be called in parallel with other Targets + +`InterruptOnError` means that the Execution gets interrupted if any of the calls return with a status code >= 400, and the next Target will not be called anymore. + +The API documentation to create a target can be found [here](/apis/resources/action_service_v2/action-service-create-target) + +### Content Signing + +To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical. + +Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v2/action-service-create-target), +and can also be newly generated when a Target is [patched](/apis/resources/action_service_v2/action-service-patch-target). + +For an example on how to check the signature, [refer to the example](/guides/integrate/actions/testing-request-signature). + +## Execution + +ZITADEL decides on specific conditions if one or more Targets have to be called. +The Execution resource contains 2 parts, the condition and the called targets. + +The condition can be defined for 4 types of processes: + +- `Requests`, before a request is processed by ZITADEL +- `Responses`, before a response is sent back to the application +- `Functions`, handling specific functionality in the logic of ZITADEL +- `Events`, after a specific event happened and was stored in ZITADEL + +The API documentation to set an Execution can be found [here](/apis/resources/action_service_v2/action-service-set-execution) + +### Condition Best Match + +As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match. +This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2.UserService` and on `/zitadel.user.v2.UserService/AddHumanUser`, +ZITADEL would with a call on the `/zitadel.user.v2.UserService/AddHumanUser` use the Executions with the following priority: + +1. `/zitadel.user.v2.UserService/AddHumanUser` +2. `zitadel.user.v2.UserService` +3. `all` + +If you then have a call on `/zitadel.user.v2.UserService/UpdateHumanUser` the following priority would be found: + +1. `zitadel.user.v2.UserService` +2. `all` + +And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used. + +### Targets and Includes + +:::info +Includes are limited to 3 levels, which mean that include1->include2->include3 is the maximum for now. +If you have feedback to the include logic, or a reason why 3 levels are not enough, please open [an issue on github](https://github.com/zitadel/zitadel/issues) or [start a discussion on github](https://github.com/zitadel/zitadel/discussions)/[start a topic on discord](https://zitadel.com/chat) +::: + +An execution can not only contain a list of Targets, but also Includes. +The Includes can be defined in the Execution directly, which means you include all defined Targets by a before set Execution. + +If you define 2 Executions as follows: + +```json +{ + "condition": { + "request": { + "service": "zitadel.user.v2.UserService" + } + }, + "targets": [ + { + "target": "" + } + ] +} +``` + +```json +{ + "condition": { + "request": { + "method": "/zitadel.user.v2.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + }, + { + "include": { + "request": { + "service": "zitadel.user.v2.UserService" + } + } + } + ] +} +``` + +The called Targets on "/zitadel.user.v2.UserService/AddHumanUser" would be, in order: + +1. `` +2. `` + +### Condition for Requests and Responses + +For Request and Response there are 3 levels the condition can be defined: + +- `Method`, handling a request or response of a specific GRPC full method, which includes the service name and method of the ZITADEL API +- `Service`, handling any request or response under a service of the ZITADEL API +- `All`, handling any request or response under the ZITADEL API + +The available conditions can be found under: +- [All available Methods](/apis/resources/action_service_v2/action-service-list-execution-methods), for example `/zitadel.user.v2.UserService/AddHumanUser` +- [All available Services](/apis/resources/action_service_v2/action-service-list-execution-services), for example `zitadel.user.v2.UserService` + +### Condition for Functions + +The available conditions can be found under [all available Functions](/apis/resources/action_service_v2/action-service-list-execution-functions). + +### Condition for Events + +For event there are 3 levels the condition can be defined: + +- Event, handling a specific event +- Group, handling a specific group of events +- All, handling any event in ZITADEL + +The concept of events can be found under [Events](/concepts/architecture/software#events) + +### Error forwarding + +If you want to forward a specific error from the Target through ZITADEL, you can provide a response from the Target with status code 200 and a JSON in the following format: + +```json +{ + "forwardedStatusCode": 403, + "forwardedErrorMessage": "Call is forbidden through the IP AllowList definition" +} +``` + +Only values from 400 to 499 will be forwarded through ZITADEL, other StatusCodes will end in a PreconditionFailed error. + +If the Target returns any other status code than >= 200 and < 299, the execution is looked at as failed, and a PreconditionFailed error is logged. diff --git a/docs/docs/guides/integrate/external-audit-log.md b/docs/docs/guides/integrate/external-audit-log.md index 93262d26f7..80dd41410d 100644 --- a/docs/docs/guides/integrate/external-audit-log.md +++ b/docs/docs/guides/integrate/external-audit-log.md @@ -20,7 +20,6 @@ The following table shows the available integration patterns for streaming audit | | Description | Self-hosting | ZITADEL Cloud | |-------------------------------------|----------------------------------------------------------------------------------------------------------------|-------------|---------------| | Events-API | Pulling events of all ZITADEL resources such as Users, Projects, Apps, etc. (Events = Change Log of Resources) | ✅ | ✅ | -| Cockroach Change Data Capture | Sending events of all ZITADEL resources such as Users, Projects, Apps, etc. (Events = Change Log of Resources) | ✅ | ❌ | | ZITADEL Actions Log to Stdout | Custom log to messages possible on predefined triggers during login / register Flow | ✅ | ❌ | | ZITADEL Actions trigger API/Webhook | Custom API/Webhook request on predefined triggers during login / register | ✅ | ✅ | @@ -34,71 +33,6 @@ This API offers granular control through various filters, enabling you to: You can find a comprehensive guide on how to use the events API for different use cases here: [Get Events from ZITADEL](/docs/guides/integrate/zitadel-apis/event-api) -### Cockroach Change Data Capture - -For self-hosted ZITADEL deployments utilizing CockroachDB as the database, [CockroachDB's built-in Change Data Capture (CDC)](https://www.cockroachlabs.com/docs/stable/change-data-capture-overview) functionality provides a streamlined approach to integrate ZITADEL audit logs with external systems. - -CDC captures row-level changes in your database and streams them as messages to a configurable destination, such as Google BigQuery or a SIEM/SOC solution. This real-time data stream enables: -- **Continuous monitoring**: Receive near-instantaneous updates on ZITADEL activity, facilitating proactive threat detection and response. -- **Simplified integration**: Leverage CockroachDB's native capabilities for real-time data transfer, eliminating the need for additional tools or configurations. - -This approach is limited to self-hosted deployments using CockroachDB and requires expertise in managing the database and CDC configuration. - -#### Sending events to Google Cloud Storage using Change Data Capture - -This example will show you how you can utilize CDC for sending all ZITADEL events to Google Cloud Storage. -For a detailed description please read [CockroachLab's Get Started Guide](https://www.cockroachlabs.com/docs/v23.2/create-and-configure-changefeeds) and [Cloud Storage Authentication](https://www.cockroachlabs.com/docs/v23.2/cloud-storage-authentication?filters=gcs#set-up-google-cloud-storage-assume-role) from Cockroach. - -You will need a Google Cloud Storage Bucket and a service account. -1. [Create Google Cloud Storage Bucket](https://cloud.google.com/storage/docs/creating-buckets) -2. [Create Service Account](https://cloud.google.com/iam/docs/service-accounts-create) -3. Create a role with the `storage.objects.create` permission -4. Grant service account access to the bucket -5. Create key for service account and download it - -Now we need to enable and create the changefeed in the cockroach DB. -1. [Enable rangefeeds on cockroach cluster](https://www.cockroachlabs.com/docs/v23.2/create-and-configure-changefeeds#enable-rangefeeds) - ```bash - SET CLUSTER SETTING kv.rangefeed.enabled = true; - ``` -2. Encode the keyfile from the service account with base64 and replace the placeholder it in the script below -3. Create Changefeed to send data into Google Cloud Storage - The following example sends all events without payload to Google Cloud Storage - Per default we do not want to send the payload of the events, as this could potentially include personally identifiable information (PII) - If you want to include the payload, you can just add `payload` to the select list in the query. - ```sql - CREATE CHANGEFEED INTO 'gs://gc-storage-zitadel-data/events?partition_format=flat&AUTH=specified&CREDENTIALS=base64encodedkey' - AS SELECT instance_id, aggregate_type, aggregate_id, owner, event_type, sequence, created_at - FROM eventstore.events2; - ``` - -In some cases you might want the payload of only some specific events. -This example shows you how to get all events and the instance domain events with the payload: - ```sql - CREATE CHANGEFEED INTO 'gs://gc-storage-zitadel-data/events?partition_format=flat&AUTH=specified&CREDENTIALS=base64encodedkey' - AS SELECT instance_id, aggregate_type, aggregate_id, owner, event_type, sequence, created_at - CASE WHEN event_type IN ('instance.domain.added', 'instance.domain.removed', 'instance.domain.primary.set' ) - THEN payload END AS payload - FROM eventstore.events2; - ``` - -The partition format in the example above is flat, this means that all files for each timestamp will be created in the same folder. -You will have files for different timestamps including the output for the events created in that time. -Each event is represented as a json row. - -Example Output: -```json lines -{ - "aggregate_id": "26553987123463875", - "aggregate_type": "user", - "created_at": "2023-12-25T10:01:45.600913Z", - "event_type": "user.human.added", - "instance_id": "123456789012345667", - "payload": null, - "sequence": 1 -} -``` - ## ZITADEL Actions ZITADEL [Actions](/docs/concepts/features/actions) offer a powerful mechanism for extending the platform's capabilities and integrating with external systems tailored to your specific requirements. diff --git a/docs/docs/guides/integrate/identity-providers/_test_setup.mdx b/docs/docs/guides/integrate/identity-providers/_test_setup.mdx index 1cdbe73d39..156f11397e 100644 --- a/docs/docs/guides/integrate/identity-providers/_test_setup.mdx +++ b/docs/docs/guides/integrate/identity-providers/_test_setup.mdx @@ -1,11 +1,11 @@

-To test the setup, use incognito mode and browse to your login page. -You see a new button which redirects you to {props.loginscreen} screen. + To test the setup, use incognito mode and browse to your login page. You see a + new button which redirects you to {props.loginscreen} screen.

By default, ZITADEL shows what you define in the default settings. If you overwrite the default settings for an organization, you need to send the organization scope in your auth request. -The organization scope looks like this: ```urn:zitadel:iam:org:id:{id}```. +The organization scope looks like this: `urn:zitadel:iam:org:id:{id}`. You can [read more about the reserved scopes](/apis/openidoauth/scopes#reserved-scopes) -or [use the ZITADEL OIDC Playground](/apis/openidoauth/authrequest) to see what happens with the login when you send different scopes. +or [use the ZITADEL OIDC Playground](https://zitadel.com/playgrounds/oidc) to see what happens with the login when you send different scopes. diff --git a/docs/docs/guides/integrate/identity-providers/keycloak.mdx b/docs/docs/guides/integrate/identity-providers/keycloak.mdx index 8eb619d8b2..1864884118 100644 --- a/docs/docs/guides/integrate/identity-providers/keycloak.mdx +++ b/docs/docs/guides/integrate/identity-providers/keycloak.mdx @@ -50,6 +50,9 @@ A useful default will be filled if you don't change anything. This information will be taken to create/update the user within ZITADEL. ZITADEL ensures that at least the `openid`-scope is always sent. +**Use PKCE**: If enabled, the provider will use Proof Key for Code Exchange (PKCE) to secure the authorization code flow +in addition to the client secret. + ![Keycloak Provider](/img/guides/zitadel_keycloak_create_provider.png) diff --git a/docs/docs/guides/integrate/identity-providers/okta-oidc.mdx b/docs/docs/guides/integrate/identity-providers/okta-oidc.mdx index f96043d941..0c2a46000a 100644 --- a/docs/docs/guides/integrate/identity-providers/okta-oidc.mdx +++ b/docs/docs/guides/integrate/identity-providers/okta-oidc.mdx @@ -49,6 +49,9 @@ A useful default will be filled if you don't change anything. This information will be taken to create/update the user within ZITADEL. ZITADEL ensures that at least the `openid`-scope is always sent. +**Use PKCE**: If enabled, the provider will use Proof Key for Code Exchange (PKCE) to secure the authorization code flow +in addition to the client secret. + ### Activate IdP diff --git a/docs/docs/guides/integrate/login-ui/device-auth.mdx b/docs/docs/guides/integrate/login-ui/device-auth.mdx new file mode 100644 index 0000000000..f60fad1310 --- /dev/null +++ b/docs/docs/guides/integrate/login-ui/device-auth.mdx @@ -0,0 +1,165 @@ +--- +title: Support for the Device Authorization Grant in a Custom Login UI +sidebar_label: Device Authorization +--- + +In case one of your applications requires the [OAuth2 Device Authorization Grant](/docs/guides/integrate/login/oidc/device-authorization) this guide will show you how to implement +this in your application as well as the custom login UI. + +The following flow shows you the different components you need to enable OAuth2 Device Authorization Grant for your login. +![Device Auth Flow](/img/guides/login-ui/device-auth-flow.png) + +1. Your application makes a device authorization request to your login UI +2. The login UI proxies the request to ZITADEL. +3. ZITADEL parses the request and does what it needs to interpret certain parameters (e.g., organization scope, etc.) +4. ZITADEL returns the device authorization response +5. Your application presents the `user_code` and `verification_uri` or maybe even renders a QR code with the `verification_uri_complete` for the user to scan +6. Your application starts a polling mechanism to check if the user has approved the device authorization request on the token endpoint +7. When the user opens the browser at the verification_uri, he can enter the user_code, or it's automatically filled in, if they scan the QR code +8. Request the device authorization request from the ZITADEL API using the user_code +9. Your login UI allows to approve or deny the device request +10. In case they approved, authenticate the user in your login UI by creating and updating a session with all the checks you need. +11. Inform ZITADEL about the decision: + 1. Authorize the device authorization request by sending the session and the previously retrieved id of the device authorization request to the ZITADEL API + 2. In case they denied, deny the device authorization from the ZITADEL API using the previously retrieved id of the device authorization request +12. Notify the user that they can close the window now and return to the application. +13. Your applications request to the token endpoint now receives the tokens or an error if the user denied the request. + +## Example + +Let's assume you host your login UI on the following URL: +``` +https://login.example.com +``` + +## Device Authorization Request + +A user opens your application and is unauthenticated, the application will create the following request: +```HTTP +POST /oauth/v2/device_authorization HTTP/1.1 +Host: login.example.com +Content-type: application/x-www-form-urlencoded + +client_id=170086824411201793& +scope=openid%20email%20profile +``` + +The request includes all the relevant information for the OAuth2 Device Authorization Grant and in this example we also have some scopes for the user. + +You now have to proxy the auth request from your own UI to the device authorization Endpoint of ZITADEL. +For more information, see [OIDC Proxy](./typescript-repo#oidc-proxy) for the necessary headers. + +:::note +The version and the optional custom URI for the available login UI is configurable under the application settings. +::: + +The endpoint will return the device authorization response: +```json +{ + "device_code": "0jbAZbU3ClK-Mkt0li4U1A", + "user_code": "FWRK-JGWK", + "verification_uri": "https://login.example.com/device", + "verification_uri_complete": "https://login.example.com/device?user_code=FWRK-JGWK", + "expires_in": 300, + "interval": 5 +} +``` + +The device presents the `user_code` and `verification_uri` or maybe even render a QR code with the `verification_uri_complete` for the user to scan. + +Your login will have to provide a page on the `verification_uri` where the user can enter the `user_code`, or it's automatically filled in, if they scan the QR code. + +### Get the Device Authorization Request by User Code + +With the user_code entered by the user you will now be able to get the information of the device authorization request. +[Get Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-get-device-authorization-request) + +```bash +curl --request GET \ + --url https://$ZITADEL_DOMAIN/v2/oidc/device_authorization/FWRK-JGWK \ + --header 'Authorization: Bearer '"$TOKEN"'' +``` + +Response Example: + +```json +{ + "deviceAuthorizationRequest": { + "id": "XzNejv6NxqVU8Qur5uxEh7f_Wi1p0qUu4PJTJ6JUIx0xtJ2uqmU", + "clientId": "170086824411201793", + "scope": [ + "openid", + "profile" + ], + "appName": "TV App", + "projectName": "My Project" + } +} +``` + +Present the user with the information of the device authorization request and allow them to approve or deny the request. + +### Perform Login + +After you have initialized the OIDC flow you can implement the login. +Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. + +Read the following resources for more information about the different checks: +- [Username and Password](./username-password) +- [External Identity Provider](./external-login) +- [Passkeys](./passkey) +- [Multi-Factor](./mfa) + +### Authorize the Device Authorization Request + +To finalize the auth request and connect an existing user session with it, you have to update the auth request with the session token. +On the create and update user session request you will always get a session token in the response. + +The latest session token has to be sent to the following request: + +Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-device-authorization) + +Make sure that the authorization header is from an account which is permitted to finalize the Auth Request through the `IAM_LOGIN_CLIENT` role. +```bash +curl --request POST \ + --url $ZITADEL_DOMAIN/v2/oidc/device_authorization/XzNejv6NxqVU8Qur5uxEh7f_Wi1p0qUu4PJTJ6JUIx0xtJ2uqmU \ + --header 'Accept: application/json' \ + --header 'Authorization: Bearer '"$TOKEN"''\ + --header 'Content-Type: application/json' \ + --data '{ + "session": { + "sessionId": "225307381909694508", + "sessionToken": "7N5kQCvC4jIf2OuBjwfyWSX2FUKbQqg4iG3uWT-TBngMhlS9miGUwpyUaN0HJ8OcbSzk4QHZy_Bvvv" + } +}' +``` + +If you don't get any error back, the request succeeded, and you can notify the user that they can close the window now and return to the application. + +### Deny the Device Authorization Request + +If the user denies the device authorization request, you can deny the request by sending the following request: + +```bash +curl --request POST \ + --url $ZITADEL_DOMAIN/v2/oidc/device_authorization/ \ + --header 'Accept: application/json' \ + --header 'Authorization: Bearer '"$TOKEN"''\ + --header 'Content-Type: application/json' \ + --data '{ + "deny": {} +}' +``` + +If you don't get any error back, the request succeeded, and you can notify the user that they can close the window now and return to the application. + +### Device Authorization Endpoints + +All OAuth2 Device Authorization Grant endpoints are provided by ZITADEL. In your login UI you just have to proxy them through and send them directly to the backend. + +These endpoints are: +- Well-known +- Device Authorization Endpoint +- Token + +Additionally, we recommend you to proxy all the other [OIDC relevant endpoints](./oidc-standard#endpoints). \ No newline at end of file diff --git a/docs/docs/guides/integrate/login-ui/external-login.mdx b/docs/docs/guides/integrate/login-ui/external-login.mdx index 3b3c47cf18..6775d2cb3b 100644 --- a/docs/docs/guides/integrate/login-ui/external-login.mdx +++ b/docs/docs/guides/integrate/login-ui/external-login.mdx @@ -16,6 +16,7 @@ ZITADEL will handle as much as possible from the authentication flow with the ex This requires you to initiate the flow with your desired provider. Send the following two URLs in the request body: + 1. SuccessURL: Page that should be shown when the login was successful 2. ErrorURL: Page that should be shown when an error happens during the authentication @@ -63,6 +64,10 @@ https://accounts.google.com/o/oauth2/v2/auth?client_id=Test&prompt=select_accoun After the user has successfully authenticated, a redirect to the ZITADEL backend /idps/callback will automatically be performed. +:::warning +Note that the redirect URL is `https://{YOUR-DOMAIN}/idps/callback` when using the new V2 hosted login compared to the V1 hosted login, which was `https://{YOUR-DOMAIN}/ui/login/login/externalidp/callback`. +::: + ## Get Provider Information ZITADEL will take the information of the provider. After this, a redirect will be made to either the success page in case of a successful login or to the error page in case of a failure will be performed. In the parameters, you will provide the IDP intentID, a token, and optionally, if a user could be found, a user ID. @@ -71,6 +76,7 @@ To get the information of the provider, make a request to ZITADEL. [Retrieve Identity Provider Intent Documentation](/docs/apis/resources/user_service_v2/user-service-retrieve-identity-provider-intent) ### Request + ```bash curl --request POST \ --url https://$ZITADEL_DOMAIN/v2/idp_intents/$INTENT_ID \ @@ -115,7 +121,9 @@ curl --request POST \ ``` ## Handle Provider Information + After successfully authenticating using your identity provider, you have three possible options. + 1. Login 2. Register user 3. Add social login to existing user @@ -127,6 +135,7 @@ Create a new session and include the IDP intent ID and the token in the checks. This check requires that the previous step ended on the successful page and didn't’t result in an error. #### Request + ```bash curl --request POST \ --url https://$ZITADEL_DOMAIN/v2/sessions \ @@ -158,6 +167,7 @@ The display name is used to list the linkings on the users. [Create User API Documentation](/docs/apis/resources/user_service_v2/user-service-add-human-user) #### Request + ```bash curl --request POST \ --url https://$ZITADEL_DOMAIN/v2/users/human \ @@ -196,6 +206,7 @@ If you want to link/connect to an existing account you can perform the add ident [Add IDP Link to existing user documentation](/docs/apis/resources/user_service_v2/user-service-add-idp-link) #### Request + ```bash curl --request POST \ --url https://$ZITADEL_DOMAIN/v2/users/users/218385419895570689/links \ diff --git a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx index e9bdfb7cbf..c96338fbf0 100644 --- a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx @@ -10,9 +10,9 @@ The following flow shows you the different components you need to enable OIDC fo ![OIDC Flow](/img/guides/login-ui/oidc-flow.png) 1. Your application makes an authorization request to your login UI -2. The login UI proxies the request to the ZITADEL API. In the request to the ZITADEL API, a header to identify your client is needed. +2. The login UI proxies the request to the ZITADEL API. 3. The ZITADEL API parses the request and does what it needs to interpret certain parameters (e.g., organization scope, etc.) -4. Redirect to a predefined, relative URL of the login UI that includes the authrequest ID ("/login?authRequest=") +4. Redirect to a predefined, relative URL of the login UI that includes the authrequest ID ("/login?authRequest="), configurable per application. 5. Request to ZITADEL API to get all the information from the auth request. This is optional and only needed if you like to get all the parsed information from the authrequest- 6. Authenticate the user in your login UI by creating and updating a session with all the checks you need. 7. Finalize the auth request by sending the session to the request, you will get the callback URL in the response @@ -37,10 +37,10 @@ https://login.example.com/oauth/v2/authorize?client_id=170086824411201793%40your The auth request includes all the relevant information for the OIDC standard and in this example we also have a login hint for the login name "minnie-mouse". You now have to proxy the auth request from your own UI to the authorize Endpoint of ZITADEL. -Make sure to add the user id of your login UI service/machine user as a header to the request: ```x-zitadel-login-client: ``` +For more information, see [OIDC Proxy](./typescript-repo#oidc-proxy) for the necessary headers. :::note -The user id sent in the 'x-zitadel-login-client' has to match to the PAT you are sending in the request. +The version and the optional custom URI for the available login UI is configurable under the application settings. ::: Read more about the [Authorize Endpoint Documentation](/docs/apis/openidoauth/endpoints#authorization_endpoint) @@ -56,7 +56,7 @@ With the ID from the redirect before you will now be able to get the information ```bash curl --request GET \ --url https://$ZITADEL_DOMAIN/v2/oidc/auth_requests/V2_224908753244265546 \ - --header 'Authorization: Bearer '"$TOKEN"''\ + --header 'Authorization: Bearer '"$TOKEN"'' ``` Response Example: @@ -90,14 +90,14 @@ Read the following resources for more information about the different checks: ### Finalize Auth Request -To finalize the auth request and connect an existing user session with it you have to update the auth request with the session token. +To finalize the auth request and connect an existing user session with it, you have to update the auth request with the session token. On the create and update user session request you will always get a session token in the response. The latest session token has to be sent to the following request: Read more about the [Finalize Auth Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-create-callback) -Make sure that the authorization header is from the same account that you originally sent in the client id header ```x-zitadel-login-client: ``` on the authorize endpoint. +Make sure that the authorization header is from an account which is permitted to finalize the Auth Request through the `IAM_LOGIN_CLIENT` role. ```bash curl --request POST \ --url $ZITADEL_DOMAIN/v2/oidc/auth_requests/V2_224908753244265546 \ @@ -128,7 +128,7 @@ Example Response: ### OIDC Endpoints -All OIDC relevant endpoints are provided by ZITADEL. In you login UI you just have to proxy them through and send them directly to the backend. +All OIDC relevant endpoints are provided by ZITADEL. In your login UI you just have to proxy them through and send them directly to the backend. These are endpoints like: - Userinfo diff --git a/docs/docs/guides/integrate/login-ui/saml-standard.mdx b/docs/docs/guides/integrate/login-ui/saml-standard.mdx index c1f282371d..8114350d5d 100644 --- a/docs/docs/guides/integrate/login-ui/saml-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/saml-standard.mdx @@ -10,9 +10,9 @@ The following flow shows you the different components you need to enable SAML fo ![SAML Flow](/img/guides/login-ui/saml-flow.png) 1. Your application makes an SAML request to your login UI -2. The login UI proxies the request to the ZITADEL API. In the request to the ZITADEL API, a header to identify your client is needed. +2. The login UI proxies the request to the ZITADEL API. 3. The ZITADEL API parses the request and does what it needs to interpret certain parameters (e.g., binding, nameID policy, etc.) -4. Redirect to a predefined, relative URL of the login UI that includes the samlrequest ID ("/login?authRequest=") +4. Redirect to a predefined, relative URL of the login UI that includes the samlrequest ID ("/login?authRequest="), configurable per application. 5. Request to ZITADEL API to get all the information from the SAML request. This is optional and only needed if you like to get all the parsed information from the samlrequest- 6. Authenticate the user in your login UI by creating and updating a session with all the checks you need. 7. Finalize the SAML request by sending the session to the request, you will get the URL to redirect to or the body in the response @@ -37,10 +37,10 @@ https://login.example.com/saml/v2/SSO?SAMLRequest=nJLRa9swEMb%2FFXHvjmVTY0fUhqxh The SAML request includes all the relevant information for the SAML standard, which includes the RelayState, the used binding and other information. You now have to proxy the SAML request from your own UI to the SSO Endpoint of ZITADEL. -Make sure to add the user id of your login UI service/machine user as a header to the request: ```x-zitadel-login-client: ``` +For more information, see [OIDC Proxy](./typescript-repo#oidc-proxy) for the necessary headers. :::note -The user id sent in the 'x-zitadel-login-client' has to match to the PAT you are sending in the request. +The version and the optional custom URI for the available login UI is configurable under the application settings. ::: Read more about the [SSO Endpoint Documentation](/docs/apis/saml/endpoints#sso_endpoint) @@ -56,7 +56,7 @@ With the ID from the redirect before you will now be able to get the information ```bash curl --request GET \ --url https://$ZITADEL_DOMAIN/v2/saml/saml_requests/V2_224908753244265546 \ - --header 'Authorization: Bearer '"$TOKEN"''\ + --header 'Authorization: Bearer '"$TOKEN"'' ``` Response Example: @@ -87,14 +87,14 @@ Read the following resources for more information about the different checks: ### Finalize SAML Request -To finalize the SAML request and connect an existing user session with it you have to update the SAML request with the session token. +To finalize the SAML request and connect an existing user session with it, you have to update the SAML Request with the session token. On the create and update user session request you will always get a session token in the response. The latest session token has to be sent to the following request: Read more about the [Finalize SAML Request Documentation](/docs/apis/resources/saml_service_v2/saml-service-create-response) -Make sure that the authorization header is from the same account that you originally sent in the client id header ```x-zitadel-login-client: ``` on the SSO endpoint. +Make sure that the authorization header is from an account which is permitted to finalize the SAML Request through the `IAM_LOGIN_CLIENT` role. ```bash curl --request POST \ --url $ZITADEL_DOMAIN/v2/saml/saml_requests/V2_224908753244265546 \ diff --git a/docs/docs/guides/integrate/login-ui/typescript-repo.mdx b/docs/docs/guides/integrate/login-ui/typescript-repo.mdx index d5fd6d9e4d..44ff9694d3 100644 --- a/docs/docs/guides/integrate/login-ui/typescript-repo.mdx +++ b/docs/docs/guides/integrate/login-ui/typescript-repo.mdx @@ -11,7 +11,6 @@ The typescript repository contains all TypeScript and JavaScript packages and ap - **[login](./typescript-repo#new-login-ui)**: The future login UI used by ZITADEL Cloud, powered by Next.js - `@zitadel/proto`: Typescript implementation of Protocol Buffers, suitable for web browsers and Node.js. - `@zitadel/client`: Core components for establishing a client connection -- `@zitadel/node`: Core components for establishing a server connection - `@zitadel/tsconfig`: shared `tsconfig.json`s used throughout the monorepo - `eslint-config-zitadel`: ESLint preset @@ -130,7 +129,6 @@ To register your login domain on your instance, [add](/docs/apis/resources/admin When setting up the new login app for OIDC, ensure it meets the following requirements: - The OIDC Proxy is deployed and running on HTTPS -- The OIDC Proxy sets `x-zitadel-login-client` which is the user ID of the service account - The OIDC Proxy sets `x-zitadel-public-host` which is the host, your login is deployed to `ex. login.example.com`. - The OIDC Proxy sets `x-zitadel-instance-host` which is the host of your instance `ex. test-hdujwl.zitadel.cloud`. @@ -138,11 +136,10 @@ You can review an example implementation of a middlware [here](https://github.co #### Deploy to Vercel -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzitadel%2Ftypescript&env=ZITADEL_API_URL,ZITADEL_SERVICE_USER_ID,ZITADEL_SERVICE_USER_TOKEN&root-directory=apps/login&envDescription=Setup%20a%20service%20account%20with%20IAM_OWNER%20membership%20on%20your%20instance%20and%20provide%20its%20id%20and%20personal%20access%20token.&project-name=zitadel-login&repository-name=zitadel-login) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzitadel%2Ftypescript&env=ZITADEL_API_URL,ZITADEL_SERVICE_USER_TOKEN&root-directory=apps/login&envDescription=Setup%20a%20service%20account%20with%20IAM_LOGIN_CLIENT%20membership%20on%20your%20instance%20and%20provide%20its%20personal%20access%20token.&project-name=zitadel-login&repository-name=zitadel-login) To deploy your own version on Vercel, navigate to your instance and create a service user. -Copy its id from the overview and set it as `ZITADEL_SERVICE_USER_ID`. -Then create a personal access token (PAT), copy and set it as `ZITADEL_SERVICE_USER_TOKEN`, then navigate to Default settings and make sure it gets `IAM_OWNER` permissions. +Create a personal access token (PAT) for the user and copy and set it as `ZITADEL_SERVICE_USER_TOKEN`, then navigate to Default settings and make sure it gets `IAM_LOGIN_CLIENT` permissions. Finally set your instance url as `ZITADEL_API_URL`. Make sure to set it without trailing slash. Also ensure your login domain is registered on your instance by adding it as a [trusted domain](/docs/apis/resources/admin/admin-service-add-instance-trusted-domain). diff --git a/docs/docs/guides/integrate/login/_prevent-lockout.mdx b/docs/docs/guides/integrate/login/_prevent-lockout.mdx new file mode 100644 index 0000000000..ee911204b6 --- /dev/null +++ b/docs/docs/guides/integrate/login/_prevent-lockout.mdx @@ -0,0 +1,4 @@ +:::caution[Don't Lock Yourself Out of Your Instance] + Before you change your Zitadel configuration, we highly recommend you to create a service user with a personal access token (PAT) and the IAM_OWNER role. + In case something breaks, you can use this PAT to revert your changes or fix the configuration so you can use a login UI again. +::: diff --git a/docs/docs/guides/integrate/login/hosted-login.mdx b/docs/docs/guides/integrate/login/hosted-login.mdx index fcb7729314..09fb86f8f0 100644 --- a/docs/docs/guides/integrate/login/hosted-login.mdx +++ b/docs/docs/guides/integrate/login/hosted-login.mdx @@ -3,6 +3,10 @@ title: Login users into your application with a hosted login UI sidebar_label: Hosted Login UI --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import PreventLockout from './_prevent-lockout.mdx'; + ZITADEL provides a hosted single-sign-on page to securely sign-in users to your applications. ZITADEL's hosted login page serves as a centralized authentication interface provided for applications that integrate ZITADEL. As a developer, understanding the hosted login page is essential for seamlessly integrating authentication into your application. @@ -134,6 +138,10 @@ In this initial release, the new login is available for self-hosting only. We'll ### Current State +:::info +The documentation describes the state of the feature in ZITADEL V3. +::: + Our primary goal for the TypeScript login system is to replace the existing login functionality within Zitadel Core, which is shipped with Zitadel automatically. This will allow us to leverage the benefits of the new system, including its modular architecture and enhanced security features. To achieve this, we are actively working on implementing the core features currently available in Zitadel Core, such as: @@ -156,15 +164,10 @@ As we continue to develop the TypeScript login system, we will provide regular u For the first implementation we have excluded the following features: -- SAML (SP & OP) - Generic JWT IDP - LDAP IDP - Device Authorization Grants -- Timebased features - - Lockout Settings - - Password Expiry Settings - - Login Settings - Multifactor init prompt - - Force MFA on external authenticated users +- Force MFA on external authenticated users - Passkey/U2F Setup - As passkey and u2f is bound to a domain, it is important to notice, that setting up the authentication possibility in the ZITADEL management console (Self-service), will not work if the login runs on a different domain - Custom Login Texts @@ -177,23 +180,46 @@ Your contributions will play a crucial role in shaping the future of our login s #### Step-by-step Guide -The simplest way to deploy the new login for yourself is by using the [“Deploy” button in our repository](https://github.com/zitadel/typescript?tab=readme-ov-file#deploy-to-vercel) to deploy the login directly to your Vercel. +**Trying out the new login:** To preview the new login without changing your current setup, the easiest way is to visit `https:///ui/v2/login` on your Zitadel Cloud instance domain. You can also activate the v2 login for your apps, so users are redirected to `/ui/v2/login` for authentication. -1. [Create a service user](https://zitadel.com/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat) (ZITADEL_SERVICE_USER_ID) with a PAT in your instance -2. Give the user IAM_LOGIN_CLIENT Permissions in the default settings (YOUR_DOMAIN/ui/console/instance?id=organizations) - Note: [Zitadel Manager Guide](https://zitadel.com/docs/guides/manage/console/managers) -3. Deploy login to Vercel: You can do so, be directly clicking the [“Deploy” button](https://github.com/zitadel/typescript?tab=readme-ov-file#deploy-to-vercel) at the bottom of the readme in our [repository](https://github.com/zitadel/typescript) -4. If you have used the deploy button in the steps before, you will automatically be asked for this step. Enter the environment variables in Vercel - - ZITADEL_SERVICE_USER_ID - - PAT - - ZITADEL_API_URL (Example: https://my-domain.zitadel.cloud, no trailing slash) -5. Add the domain where your login UI is hosted to the [trusted domains](https://zitadel.com/docs/apis/resources/admin/admin-service-add-instance-trusted-domain) in Zitadel. (Example: my-new-zitadel-login.vercel.app) -6. Use the new login in your application. You have three different options on how to achieve this - 1. Enable the new login on your application configuration and add the URL of your login UI, with that settings Zitadel will automatically redirect you to the new login if you call the old one. - ![Login V2 Application Configuration](/img/guides/integrate/login/login-v2-app-config.png) - 2. Enable the [loginV2 feature](https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) on the instance and add the URL of your login. If you enable this feature, the login will be used for every application configured in your Zitadel instance. (Example: https://my-new-zitadel-login.vercel.app) - 3. Change the issuer in the code of your application to the new domain of your login -7. Enforce users to have their email verified. By setting `EMAIL_VERIFICATION` to `true` in your environment variables, your users will be enforced to verify their email address before they can log in. +**Customizing the new login:** The easiest way to actually customizing it is to fork the https://github.com/zitadel/typescript repo and use the "Deploy" button to run your code on Vercel. + + + + + Zitadel Cloud provides the V2 login at the fixed path `/ui/v2/login`. + + + To activate it to authenticate on your Zitadel Cloud apps, you can follow one of these steps: + + 1. Enable the new login on your application configuration. Leave the field **Custom base URL for the new Login UI** empty to use the default. With these settings, Zitadel will automatically redirect you to the new login if you call the old one. + ![Login V2 Application Configuration](/img/guides/integrate/login/login-v2-app-config.png) + 2. Enable the [loginV2 feature](https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) on the instance. Leave the base URI empty to use the default. If you enable this feature, the login will be used for every application configured in your Zitadel instance. (Example: https://your-zitadel-instance.zitadel.cloud/ui/v2/login) + + + + + The simplest way to deploy the new login for yourself is by using the [“Deploy” button in our repository](https://github.com/zitadel/typescript?tab=readme-ov-file#deploy-to-vercel) to deploy the login directly to your Vercel. + + 1. [Create a service user](https://zitadel.com/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat) with a PAT in your instance + 2. Give the user IAM_LOGIN_CLIENT Permissions in the default settings (YOUR_DOMAIN/ui/console/instance?id=organizations) + Note: [Zitadel Manager Guide](https://zitadel.com/docs/guides/manage/console/managers) + 3. Deploy login to Vercel: You can do so by directly clicking the [“Deploy” button](https://github.com/zitadel/typescript?tab=readme-ov-file#deploy-to-vercel) at the bottom of the readme in our [repository](https://github.com/zitadel/typescript) + 4. If you have used the deploy button in the steps before, you will automatically be asked for this step. Enter the environment variables in Vercel + - PAT + - ZITADEL_API_URL (Example: https://my-domain.zitadel.cloud, no trailing slash) + 5. Add the domain where your login UI is hosted to the [trusted domains](https://zitadel.com/docs/apis/resources/admin/admin-service-add-instance-trusted-domain) in Zitadel. (Example: my-new-zitadel-login.vercel.app) + 6. Use the new login in your application. + + You have three different options on how to achieve this: + 1. Enable the new login on your application configuration and add the URL of your login UI. With these settings, Zitadel will automatically redirect you to the new login if you call the old one. + ![Login V2 Application Configuration](/img/guides/integrate/login/login-v2-app-config.png) + 2. Enable the [loginV2 feature](https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) on the instance and add the URL of your login. If you enable this feature, the login will be used for every application configured in your Zitadel instance. (Example: https://my-new-zitadel-login.vercel.app) + 3. Change the issuer in the code of your application to the new domain of your login + 7. Enforce users to have their email verified. By setting `EMAIL_VERIFICATION` to `true` in your environment variables, your users will be enforced to verify their email address before they can log in. + + + ### Important Notes diff --git a/docs/docs/guides/integrate/login/login-users.mdx b/docs/docs/guides/integrate/login/login-users.mdx index e80e70c5b7..de1dacfe24 100644 --- a/docs/docs/guides/integrate/login/login-users.mdx +++ b/docs/docs/guides/integrate/login/login-users.mdx @@ -20,7 +20,7 @@ In this guide, we will walk through the different protocols, features and concep OpenID Connect (OIDC) offers a modern and lightweight authentication protocol built on top of OAuth 2.0, providing flexible authentication flows and easy integration with web and mobile applications. ZITADEL offers a certified compliant implementation of the OpenID Connect Standard, ensuring compliance with proven security best practices. -Authenticating users through the OpenID Connect protocol typically requires an application to redirect the user with an [Auth Request](/docs/apis/openidoauth/authrequest) to the identity provider that contains information such as the requesting application, [scopes](/docs/apis/openidoauth/scopes), and redirect url. +Authenticating users through the OpenID Connect protocol typically requires an application to redirect the user with an [Auth Request](https://zitadel.com/playgrounds/oidc) to the identity provider that contains information such as the requesting application, [scopes](/docs/apis/openidoauth/scopes), and redirect url. The identity provider is not part of the original application, but a standalone service like ZITADEL that may run under the [same domain](/docs/concepts/features/custom-domain.md) The user will authenticate using their credentials. After successful authentication, the user will be redirected back to the original application. @@ -93,6 +93,7 @@ This centralized authentication interface simplifies application integration by For a comprehensive understanding of the hosted login page and its capabilities, please refer to our [dedicated guide](/docs/guides/integrate/login/hosted-login) The hosted login is particularly well-suited for scenarios where: + - **Minimal branding is required:** If your primary focus is on functionality over a highly customized look and feel. - **Standard authentication flows suffice:** Your application doesn't necessitate complex or unique authentication processes. - **OIDC or SAML are suitable:** Your application integrates seamlessly with industry-standard protocols. diff --git a/docs/docs/guides/integrate/login/oidc/authmethods/_basic.mdx b/docs/docs/guides/integrate/login/oidc/authmethods/_basic.mdx index 5f4fbfbb5b..4833ae29a5 100644 --- a/docs/docs/guides/integrate/login/oidc/authmethods/_basic.mdx +++ b/docs/docs/guides/integrate/login/oidc/authmethods/_basic.mdx @@ -21,7 +21,7 @@ the authentication process. The latter is used to bind the client session with t You don't need any additional parameter for this request. We're identifying the app by the `client_id` parameter. -Try out the request in our [OIDC Authentication Request Playground](/apis/openidoauth/authrequest?auth_method=Client%20Secret%20Basic). +Try out the request in our [OIDC Authentication Request Playground](https://zitadel.com/playgrounds/oidc?auth_method=Client%20Secret%20Basic). ### Additional parameters and customization diff --git a/docs/docs/guides/integrate/login/oidc/authmethods/_jwtpk.mdx b/docs/docs/guides/integrate/login/oidc/authmethods/_jwtpk.mdx index 037df10d90..9efd8188ae 100644 --- a/docs/docs/guides/integrate/login/oidc/authmethods/_jwtpk.mdx +++ b/docs/docs/guides/integrate/login/oidc/authmethods/_jwtpk.mdx @@ -23,7 +23,7 @@ You don't need any additional parameter for this request. We're identifying the So your request might look like this (linebreaks and whitespace for display reasons): -Try out the request in our [OIDC Authentication Request Playground](/apis/openidoauth/authrequest?auth_method=Client%20Secret%20Basic). +Try out the request in our [OIDC Authentication Request Playground](https://zitadel.com/playgrounds/oidc?auth_method=Client%20Secret%20Basic). ### Additional parameters and customization diff --git a/docs/docs/guides/integrate/login/oidc/authmethods/_pkce.mdx b/docs/docs/guides/integrate/login/oidc/authmethods/_pkce.mdx index 708eecc01a..aea1256bbc 100644 --- a/docs/docs/guides/integrate/login/oidc/authmethods/_pkce.mdx +++ b/docs/docs/guides/integrate/login/oidc/authmethods/_pkce.mdx @@ -29,7 +29,7 @@ the hash as well and to verify it's correct. In order to do so you're required t For example for `random-string` the code_challenge would be `9az09PjcfuENS7oDK7jUd2xAWRb-B3N7Sr3kDoWECOY` -Try out the request in our [OIDC Authentication Request Playground](/apis/openidoauth/authrequest). +Try out the request in our [OIDC Authentication Request Playground](https://zitadel.com/playgrounds/oidc). ### Additional parameters and customization diff --git a/docs/docs/guides/integrate/login/oidc/webkeys.md b/docs/docs/guides/integrate/login/oidc/webkeys.md index 2b414ae7e9..a66cae61a9 100644 --- a/docs/docs/guides/integrate/login/oidc/webkeys.md +++ b/docs/docs/guides/integrate/login/oidc/webkeys.md @@ -23,6 +23,7 @@ endpoints are called with a JWT access token. :::info Web keys are an [experimental](/docs/support/software-release-cycles-support#beta) feature. Be sure to enable the `web_key` [feature](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) before using it. +The documentation describes the state of the feature in ZITADEL V3. Test the feature and add improvement or bug reports directly to the [github repository](https://github.com/zitadel/zitadel) or let us know your general feedback in the [discord thread](https://discord.com/channels/927474939156643850/1329100936127320175/threads/1332344892629717075)! ::: @@ -112,7 +113,7 @@ When the request does not contain any specific configuration, [RSA](#rsa) is used as default with the default options as described below: ```bash -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -136,7 +137,7 @@ The RSA generator config takes two enum values. For example, to create a RSA web key with the size of 3072 bits and the SHA512 algorithm (RS512): ```bash -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -160,7 +161,7 @@ The ECDSA generator config takes a single `curve` enum value which determines bo For example, to create a ECDSA web key with a P-256 curve and the SHA256 algorithm: ```bash -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -184,7 +185,7 @@ Clients which support both curves must inspect `crv` header value to assert the For example, to create a ed25519 web key: ```bash -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -246,11 +247,11 @@ For the sake of this example we will use simplified IDs and restrict timestamps After one month, on 2025-02-01, we wish to activate the next available key and create a new key to be available for activation next month. This fulfills requirements 1 and 2. ```bash -curl -L -X POST 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys/2/_activate' \ +curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/web_keys/2/_activate' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' -curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \ +curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -282,7 +283,7 @@ In addition to the activate and create calls we made on this iteration, we can now safely delete the oldest key, as both requirement 3 and 4 are now fulfilled: ```bash -curl -L -X DELETE 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys/1' \ +curl -L -X DELETE 'https://$CUSTOM-DOMAIN/v2beta/web_keys/1' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` diff --git a/docs/docs/guides/integrate/onboarding/end-users.mdx b/docs/docs/guides/integrate/onboarding/end-users.mdx index 4c720be041..d8cf46e2c7 100644 --- a/docs/docs/guides/integrate/onboarding/end-users.mdx +++ b/docs/docs/guides/integrate/onboarding/end-users.mdx @@ -4,6 +4,7 @@ sidebar_label: Onboard Users --- End Users have three different possibilities on how to login with ZITADEL. + 1. Local Account with Username, Password, MFA, Passkey, etc 2. Social Login like Google, Apple, Github, etc 3. External Identity Provider hosted/managed by Organization like Entra ID, LDAP, Okta etc @@ -12,9 +13,9 @@ You can either use the hosted login of ZITADEL to let users register themselves, ## Manually add/invite users -import CreateUser from '/docs/guides/manage/console/_create-user.mdx'; +import CreateUser from "/docs/guides/manage/console/_create-user.mdx"; - + ## Automated / Self-registration possibilities @@ -29,15 +30,15 @@ By default, redirecting from your application displays the login page. OpenID Connect (OIDC) allows you to control the initial screen by sending a [prompt](/docs/apis/openidoauth/endpoints#additional-parameters) parameter in the authorization request. With the `prompt=create`, the registration form/options will directly be shown to the user. -You can test the impact of the different prompts on your login UI in our [OIDC Playground](/docs/apis/openidoauth/authrequest). +You can test the impact of the different prompts on your login UI in our [OIDC Playground](https://zitadel.com/playgrounds/oidc). Per default a user will be registered to the default organization. By sending the scope below in your authorization request you can choose the organization to which the user will be added. + ``` urn:zitadel:iam:org:id:{id} ``` - Unfortunately, SAML doesn't offer the same level of control over the initial screen. You won't be able to directly influence which page (login or registration) is shown through the SAML flow. @@ -47,7 +48,10 @@ If an organization allows local user registration as well as registration with a As soon as users click the "register" button, they will be presented with a screen showing the different registration options. -Register Options +Register Options After that, the user can select either local user registration or an external provider. By pressing the button of an external provider, the user will directly be redirected to the provider for consent. @@ -62,14 +66,22 @@ If only one option is possible, the option will directly be selected and shown. To allow users to register themselves, you have to enable the "register allowed" in the login behavior settings. You will now see the register button on the login screen. -Register End User +Register End User If nothing else is specified, a user will be registered to the default organization. -Default Organization + +Default Organization You can specify another organization, by sending the organization scope in the authorization requests. By sending the scope below the settings of the specified organization will be triggered and only users of the said organization will be able to authenticate. The users will be registered to the given organization. + ``` urn:zitadel:iam:org:id:{id} ``` @@ -78,7 +90,10 @@ If the user chooses to register a local account, the register form will be shown All the mandatory fields like given name, family name, e-mail and password have to be filled. You can only setup authentication with the built-in form. -Register local user +Register local user #### Registration with Social Login @@ -91,7 +106,10 @@ Please follow the configuration guides for the needed providers: [Let Users Logi The configured providers will be shown on the first login screen or when the users click on the registration button, they will be able to choose between local account or the social login. -Register End User +Register End User #### Registration with Organization External Identity Provider @@ -110,6 +128,7 @@ ZITADEL allows you to build your own registration form and login UI. The registration process highly depends on your needs. We do have a guide series on how to build your own login ui, which also includes the registration of different authentication methods, such as: + - Password authentication - Multi-Factor - Passkeys @@ -127,4 +146,4 @@ We recommend storing business relevant data in the database of your application, #### Registration with Organization External Identity Provider If you want to know more about the multi-tenancy possibilities of ZITADEL, read the following blog post: -[Multi-Tenancy and Delegated Access Management with Organizations](https://zitadel.com/blog/multi-tenancy-with-organizations) \ No newline at end of file +[Multi-Tenancy and Delegated Access Management with Organizations](https://zitadel.com/blog/multi-tenancy-with-organizations) diff --git a/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md b/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md index 4e2b0b9973..4b96e9bbd5 100644 --- a/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md +++ b/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md @@ -16,6 +16,8 @@ To authenticate the user a self-signed JWT will be created and utilized. You can define any id for your user. This guide will assume it's `system-user-1`. +**NOTE:** system user id cannot contain capital letters + ## Generate an RSA keypair Generate an RSA private key with 2048 bit modulus: diff --git a/docs/docs/guides/manage/cloud/billing.md b/docs/docs/guides/manage/cloud/billing.md index 561986530c..0804d7a943 100644 --- a/docs/docs/guides/manage/cloud/billing.md +++ b/docs/docs/guides/manage/cloud/billing.md @@ -1,14 +1,7 @@ --- -title: Settings / Billing +title: Billing --- -## General - -In the general settings you can change your team name, notification settings and delete your team. - -![Customer Portal Settings General](/img/manuals/portal/customer_portal_settings_general.png) - - ## Billing In the billing page shows your configured payment methods and the invoice diff --git a/docs/docs/guides/manage/cloud/instances.md b/docs/docs/guides/manage/cloud/instances.md index 7bb3f16640..9fbc740521 100644 --- a/docs/docs/guides/manage/cloud/instances.md +++ b/docs/docs/guides/manage/cloud/instances.md @@ -54,9 +54,8 @@ To upgrade you must enter your billing information. If you hit a limit from the free tier you will automatically be asked to add your credit card information and to subscribe to the pro tier. You can also upgrade manually at any time. -1. Go to the settings tab -2. You can now see your Plan: "FREE" -3. Click "Upgrade" +1. Click the "Upgrade to PRO" button in the menu or go to the billing menu +2. If you choose the billing menu, you can now see your Free plan, click "Upgrade to Pro" 4. Add the missing data - Payment method: Credit Card Information - Customer: At least you have to fill the country @@ -70,7 +69,7 @@ We recommend register a custom domain to access your ZITADEL instance. The primary custom domain of your ZITADEL instance will be the issuer of the instance. All other custom domains can be used to access the instance itself 1. Browse to the "Custom Domains" Tab -2. Click **Add** +2. Click **Add domain** 3. Enter the domain you want and select the instance where the domain should belong to 4. In the next screen you will get all the information you will have to add to your DNS provider to verify your domain diff --git a/docs/docs/guides/manage/cloud/settings.md b/docs/docs/guides/manage/cloud/settings.md index 5b646db3e1..fe0ee9e752 100644 --- a/docs/docs/guides/manage/cloud/settings.md +++ b/docs/docs/guides/manage/cloud/settings.md @@ -2,7 +2,10 @@ title: Settings --- -Manage your team, email subscriptions, and billing information on the [Settings](https://zitadel.com/admin/settings) page. + +In the settings you can change your team name, notification settings and delete your team. + +![Customer Portal Settings General](/img/manuals/portal/customer_portal_settings_general.png) ## Team name diff --git a/docs/docs/guides/manage/console/_create-user.mdx b/docs/docs/guides/manage/console/_create-user.mdx index ce9141af72..3434284de2 100644 --- a/docs/docs/guides/manage/console/_create-user.mdx +++ b/docs/docs/guides/manage/console/_create-user.mdx @@ -3,9 +3,34 @@ To create a new user, go to Users and click on **New**. Enter the required conta import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; +:::note +If you started with Zitadel before version 3, you might have the "Human User [deprecated]" UI. +In this case please enable the Feature Flag "Use V2 Api in Console for User creation" in the Default Settings. +::: + + Invite Human + + When creating a new user you have different options. + First add the email, and select if the email address should be added automatically as "verified". + + In the last section you can choose the authentication options: + - **Setup authentication later for this user**: This flow might be useful if an employee starts at a later point but you already want to prepare the account. The user will not have an authentication method, before they will be able to login, they need to setup a method. + - **Send an invitation E-Mail for authentication setup and E-Mail verification**: The user will receive an email and be able to setup an authentication method (e.g Password, Passkey, External SSO). + - When using the [Zitadel Login V1](/docs/guides/integrate/login/hosted-login) the user will be prompted to setup a password + - When using the [Zitadel Login V2](/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) the user has the option to choose the authentication method (password, passkey, identity provider), based on the configuration of the organization + Invite Human - Setup authentication method + - **Set an initial password for the user**: The user will receive an email and be able to setup an authentication method (e.g Password, Passkey, External SSO) + + + Add Human + + After a human user is created, by default, an initialization mail with a code is sent to the registered email. This code then has to be verified on first login. + If you want to omit this mail, you can check the **email verified** and **set initial password** toggle. + If no password is set initially, the initialization mail prompting the user to set his password is sent. + -After a human user is created, by default, an initialization mail with a code is sent to the registered email. This code then has to be verified on first login. -If you want to omit this mail, you can check the **email verified** and **set initial password** toggle. -If no password is set initially, the initialization mail prompting the user to set his password is sent. - You can prompt the user to add a second factor method too by checking the **Force MFA** toggle in [Login behaviour settings](/docs/guides/manage/console/default-settings#login-behavior-and-access). When logged in, a user can then manage the profile in the console, adding a profile picture, external IDPs and Passwordless authentication devices. diff --git a/docs/docs/guides/manage/console/default-settings.mdx b/docs/docs/guides/manage/console/default-settings.mdx index 26639b32f3..e8e36956a1 100644 --- a/docs/docs/guides/manage/console/default-settings.mdx +++ b/docs/docs/guides/manage/console/default-settings.mdx @@ -5,7 +5,7 @@ sidebar_label: Default Settings Default settings work as default or fallback settings for your organizational settings. Most of the time you only have to set default settings for the cases where you don't need specific behavior in the organizations themselves or you only have one organization. -To access default settings, use the settomgs page at `{instanceDomain}/ui/console/settings` or click at the default settings button on the **top-right** of the page and then navigate to settings in the navigation. +To access default settings, use the settings page at `{instanceDomain}/ui/console/settings` or click at the default settings button on the **top-right** of the page and then navigate to settings in the navigation. /ui/console/` +If there is no [auth request](https://zitadel.com/playgrounds/oidc), users will be redirected to the Default Redirect URI, which is by default `https:///ui/console/` Reasons why ZITADEL doesn't have a redirect URI: diff --git a/docs/docs/guides/manage/console/managers.mdx b/docs/docs/guides/manage/console/managers.mdx index 59d4a02bfb..626768ed97 100644 --- a/docs/docs/guides/manage/console/managers.mdx +++ b/docs/docs/guides/manage/console/managers.mdx @@ -26,6 +26,7 @@ import AddManager from "./_add_manager.mdx"; | IAM User Manager | IAM_USER_MANAGER | Manage all users and their authorizations over all organizations | | IAM Admin Impersonator | IAM_ADMIN_IMPERSONATOR | Allow impersonation of admin and end users from all organizations | | IAM Impersonator | IAM_END_USER_IMPERSONATOR | Allow impersonation of end users from all organizations | +| IAM Login Client | IAM_LOGIN_CLIENT | Get all permissions needed to implement your own Login UI. | | Org Owner | ORG_OWNER | Manage everything within an organization | | Org Owner Viewer | ORG_OWNER_VIEWER | View everything within an organization | | Org User Manager | ORG_USER_MANAGER | Manage users and their authorizations within an organization | diff --git a/docs/docs/guides/manage/console/organizations.mdx b/docs/docs/guides/manage/console/organizations.mdx index 924c02d045..d9336421e9 100644 --- a/docs/docs/guides/manage/console/organizations.mdx +++ b/docs/docs/guides/manage/console/organizations.mdx @@ -131,7 +131,7 @@ This means when you want to trigger the settings of an organization directly, ma urn:zitadel:iam:org:id:{id} ``` -Read more about the [scopes](/docs/apis/openidoauth/scopes#reserved-scopes) or try it out in our [OIDC Playground](/docs/apis/openidoauth/authrequest). +Read more about the [scopes](/docs/apis/openidoauth/scopes#reserved-scopes) or try it out in our [OIDC Playground](https://zitadel.com/playgrounds/oidc). ## Default organization diff --git a/docs/docs/guides/manage/customize/branding.md b/docs/docs/guides/manage/customize/branding.md index b6ffbf909d..14c18705f6 100644 --- a/docs/docs/guides/manage/customize/branding.md +++ b/docs/docs/guides/manage/customize/branding.md @@ -24,13 +24,13 @@ Upload your logo for the chosen theme, as soon as it is uploaded the preview on ### Colors -In the next part you can configure your colors. -Background colour is self-explanatory, the primary color will be used for buttons, links and some highlights. -The warn color is used for all the error messages and warnings and the font colour for texts. +In the next part you can configure your colors. +Background colour is self-explanatory, the primary color will be used for buttons, links and some highlights. +The warn color is used for all the error messages and warnings and the font colour for texts. ### Font -Last step to apply to your branding is the font upload. +Last step to apply to your branding is the font upload. The best way is to upload a ttf file after a successful upload you will see it in the font part, but not in the preview. ### Advanced Settings @@ -46,7 +46,7 @@ If you like to trigger your settings for your applications you have different po Send a [reserved scope](/apis/openidoauth/scopes) with your [authorization request](../../integrate/login/oidc/login-users#auth-request) to trigger your organization. The primary domain scope will restrict the login to your organization, so only users of your own organization will be able to login. -You can use our [OpenID Authentication Request Playground](/apis/openidoauth/authrequest) to learn more about how to trigger an [organization's policies and branding](/apis/openidoauth/authrequest#organization-policies-and-branding). +You can use our [OpenID Authentication Request Playground](/oidc-playground) to learn more about how to trigger an [organization's policies and branding](/oidc-playground#organization-policies-and-branding). ### 2. Setting on your Project diff --git a/docs/docs/guides/manage/customize/texts.md b/docs/docs/guides/manage/customize/texts.md index 0d4bfdd21e..bd3cb6ea90 100644 --- a/docs/docs/guides/manage/customize/texts.md +++ b/docs/docs/guides/manage/customize/texts.md @@ -52,6 +52,7 @@ ZITADEL is available in the following languages - Swedish (sv) - Hungarian (hu) - 한국어 (ko) +- Romanian (ro) A language is displayed based on your agent's language header. If a users language header doesn't match any of the supported or [restricted](#restrict-languages) languages, the instances default language will be used. diff --git a/docs/docs/guides/manage/customize/user-metadata.md b/docs/docs/guides/manage/customize/user-metadata.md index 0738b7f0c9..087956d39e 100644 --- a/docs/docs/guides/manage/customize/user-metadata.md +++ b/docs/docs/guides/manage/customize/user-metadata.md @@ -24,7 +24,7 @@ You can do so by using [Console](../console/users) or [setting user metadata](/d Most of the methods below require you to login with the correct user while setting some scopes. Make sure you pick the right user when logging into the test application. -Use the [OIDC authentication request playground](/docs/apis/openidoauth/authrequest) or the configuration of an [example client](/docs/sdk-examples/introduction) to set the required scopes and receive a valid access token. +Use the [OIDC authentication request playground](https://zitadel.com/playgrounds/oidc) or the configuration of an [example client](/docs/sdk-examples/introduction) to set the required scopes and receive a valid access token. :::info Getting a token In case you want to test out different settings configure an application with code flow (PKCE). @@ -54,27 +54,27 @@ curl --request GET \ --header "Authorization: Bearer $ACCESS_TOKEN" ``` -Replace `$ACCESS_TOKEN` with your user's access token. +Replace `$ACCESS_TOKEN` with your user's access token. The response will look something like this ```json { - "email":"road.runner@zitadel.com", - "email_verified":true, - "family_name":"Runner", - "given_name":"Road", - "locale":"en", - "name":"Road Runner", - "preferred_username":"road.runner@...asd.zitadel.cloud", - "sub":"166.....729", - "updated_at":1655467738, - //highlight-start - "urn:zitadel:iam:user:metadata":{ - "ContractNumber":"MTIzNA", - } - //highlight-end - } + "email": "road.runner@zitadel.com", + "email_verified": true, + "family_name": "Runner", + "given_name": "Road", + "locale": "en", + "name": "Road Runner", + "preferred_username": "road.runner@...asd.zitadel.cloud", + "sub": "166.....729", + "updated_at": 1655467738, + //highlight-start + "urn:zitadel:iam:user:metadata": { + "ContractNumber": "MTIzNA" + } + //highlight-end +} ``` You can grab the metadata from the reserved claim `"urn:zitadel:iam:user:metadata"` as key-value pairs. @@ -95,10 +95,10 @@ The result will give you something like: ```json { - "access_token":"jZuRixKQTVecEjKqw...kc3G4", - "token_type":"Bearer", - "expires_in":43199, - "id_token":"ey...Ww" + "access_token": "jZuRixKQTVecEjKqw...kc3G4", + "token_type": "Bearer", + "expires_in": 43199, + "id_token": "ey...Ww" } ``` @@ -106,12 +106,7 @@ When you decode the value of `id_token`, then the response will include the meta ```json { - "amr": [ - "password", - "pwd", - "mfa", - "otp" - ], + "amr": ["password", "pwd", "mfa", "otp"], "at_hash": "lGIblkTr8faHz2zd0oTddA", "aud": [ "170086824411201793@portal", @@ -157,7 +152,7 @@ You can use the authentication service to request and search for the user's meta The introspection endpoint and the token endpoint in the examples above do not require a special scope to access. Yet when accessing the authentication service, you need to pass the [reserved scope](/docs/apis/openidoauth/scopes#reserved-scopes) `urn:zitadel:iam:org:project:id:zitadel:aud` along with the authentication request. This scope allows the user to access ZITADEL's APIs, specifically the authentication API that we need for this method. -Use the [OIDC authentication request playground](/docs/apis/openidoauth/authrequest) or the configuration of an [example client](/docs/sdk-examples/introduction) to set the required scopes and receive a valid access token. +Use the [OIDC authentication request playground](https://zitadel.com/playgrounds/oidc) or the configuration of an [example client](/docs/sdk-examples/introduction) to set the required scopes and receive a valid access token. :::note Invalid audience If you get the error "invalid audience (APP-Zxfako)", then you need to add the reserved scope `urn:zitadel:iam:org:project:id:zitadel:aud` to your authentication request. @@ -199,23 +194,23 @@ An example response for your search looks like this: ```json { - "details":{ - "totalResult":"1", - "processedSequence":"2935", - "viewTimestamp":"2023-06-21T16:01:52.829838Z" - }, - "result":[ - { - "details":{ - "sequence":"409", - "creationDate":"2022-08-04T09:09:06.259324Z", - "changeDate":"2022-08-04T09:09:06.259324Z", - "resourceOwner":"170086363054473473" - }, - "key":"ContractNumber", - "value":"MTIzNA" - } - ] + "details": { + "totalResult": "1", + "processedSequence": "2935", + "viewTimestamp": "2023-06-21T16:01:52.829838Z" + }, + "result": [ + { + "details": { + "sequence": "409", + "creationDate": "2022-08-04T09:09:06.259324Z", + "changeDate": "2022-08-04T09:09:06.259324Z", + "resourceOwner": "170086363054473473" + }, + "key": "ContractNumber", + "value": "MTIzNA" + } + ] } ``` diff --git a/docs/docs/guides/migrate/introduction.md b/docs/docs/guides/migrate/introduction.md index 2955f91d4f..6d18fa65af 100644 --- a/docs/docs/guides/migrate/introduction.md +++ b/docs/docs/guides/migrate/introduction.md @@ -16,8 +16,7 @@ Multi-tenancy in ZITADEL can be achieved through either [Instances](/docs/concep Where instances represent isolated ZITADEL instances, Organizations provide a more permeable approach to multi-tenancy. In most cases, when you want to achieve multi-tenancy, you use Organizations. Each organization can have their own set of Settings (eg, Security Policies, IDPs, Branding), Managers, and Users. -Please also consult our guide on [Solution Scenarios](/docs/guides/solution-scenarios/introduction -) for B2C and B2B for more details. +Please also consult our guide on [Solution Scenarios](/docs/guides/solution-scenarios/introduction) for B2C and B2B for more details. ## Delegated access management @@ -76,7 +75,7 @@ In case all your applications depend on ZITADEL after the migration date, and ZI For all other cases, we recommend that the **legacy system orchestrates the migration** of users to ZITADEL for more flexibility: - Update your legacy system to create a user in ZITADEL on their next login, if not already flagged as migrated, by using our APIs (you can set the password and a verified email) -- Redirect migrated users with a login hint in the [auth request](/docs/apis/openidoauth/authrequest.mdx) to ZITADEL to pre-select the user +- Redirect migrated users with a login hint in the [auth request](https://zitadel.com/playgrounds/oidc) to ZITADEL to pre-select the user In this case the migration can also be done as an import job or also allowing to create user session in both the legacy auth solution and ZITADEL in parallel with identity brokering: diff --git a/docs/docs/guides/solution-scenarios/domain-discovery.mdx b/docs/docs/guides/solution-scenarios/domain-discovery.mdx index e03c42bde4..712833f06d 100644 --- a/docs/docs/guides/solution-scenarios/domain-discovery.mdx +++ b/docs/docs/guides/solution-scenarios/domain-discovery.mdx @@ -31,7 +31,7 @@ When opening `login.mycompany.com` then the login policy of the instance will be This means that you have to configure the [Login and Access](/docs/guides/manage/console/default-settings#login-behavior-and-access) Policy and [Identity Providers](/docs/guides/manage/console/default-settings#identity-providers) for the **CIAM** users on the instance itself. :::info -You can also configure these settings on the default organization (see below) and send the scope `urn:zitadel:iam:org:id:{id}` with every [auth request](/docs/apis/openidoauth/authrequest#organization-policies-and-branding). +You can also configure these settings on the default organization (see below) and send the scope `urn:zitadel:iam:org:id:{id}` with every [auth request](https://zitadel.com/playgrounds/oidc). ::: ### Default Organization diff --git a/docs/docs/guides/start/quickstart.mdx b/docs/docs/guides/start/quickstart.mdx index b5c3ff5d6d..30818a75b5 100644 --- a/docs/docs/guides/start/quickstart.mdx +++ b/docs/docs/guides/start/quickstart.mdx @@ -103,25 +103,35 @@ The order of creation for the above components may vary depending on the specifi ![Login](/img/guides/quickstart/v3_8.png) -### 2. Create your first instance +### 2. Complete onboarding questions -As a user of the ZITADEL Cloud Customer Portal, you now can create multiple instances to suit your specific needs. This includes instances for development, production, or user acceptance testing, as well as instances for different clients or applications. For example, you might create an instance for each product in a B2C scenario, or an instance for each tenant or customer in a B2B scenario. The possibilities are endless. You can create your first instance for free. +To begin, we'll ask you a few questions. +These will help us understand your needs and personalize your ZITADEL experience, ensuring you get the most out of it. -1. Let’s create an instance. Click on “Create new instance”. +![Onboarding Questions](/img/guides/quickstart/onboarding_questions.png) -![2-factor Authentication](/img/guides/quickstart/v3_9.png) +### 3. Create your first instance + +As a user of the ZITADEL Cloud Customer Portal, you now can create multiple instances to suit your specific needs. +This includes instances for development, production, or user acceptance testing, as well as instances for different clients or applications. +The possibilities are endless. You can create your first instance for free. + +1. Create an instance. +by clicking on “Create Instance”. + +![Create Instance](/img/guides/quickstart/create_instance.png) 2. Provide a name for your instance and click on the “Continue” button at the bottom of this screen. -![Select Tier](/img/guides/quickstart/v3_10.png) +![Create Instance Name](/img/guides/quickstart/create_instance_name.png) -3. Next, you should see the following screen. Add a username and password for the instance manager and click "Create". +3. Next, you should see the following screen. Add a username and password for the instance administrator and click "Create". -![Instance Details](/img/guides/quickstart/v3_11.png) +![Create Instance Admin](/img/guides/quickstart/create_instance_admin.png) -3. Now you will see the details of your first instance. You can click on "Visit" at the top right to go to your instance. +3. After creating your first instance, you're ready to configure user authentication. To access the management console, click the 'Sign in to your instance' button. -![Instance Details](/img/guides/quickstart/v3_12.png) +![Sign In Instance](/img/guides/quickstart/sign_in_instance.png) 6. To log in to your instance, provide the username and password you set in the instance creation, and click “next”. @@ -129,23 +139,33 @@ As a user of the ZITADEL Cloud Customer Portal, you now can create multiple inst 8. And there you go! You now have access to your instance. -![Instance Details](/img/guides/quickstart/v3_16.png) +![Instance Details](/img/guides/quickstart/instance_dashboard.png) -### 3. Create your first project +### 4. Create your first project and app -1. To create a project in the instance you just created, click on "Projects" in the navigation and then “Create a project”. +Because applications are always associated with a project, we'll guide you through setting up both in the following steps. -2. Insert “Project1” (or any name of your choice) as the project’s name and click the “Continue” button. +1. Click the "Create Application" button or the Framework/Language of your choice on the right side -![Project Name](/img/guides/quickstart/20.png) +2. Insert “Project1” (or any name of your choice) as the project’s name and select the framework/language of your choice in our case "React" and click the “Continue” button. (If you can't find your framework/languages select "Other") -3. Now you will be taken to the screen below. “Project1” has been created. +![Project Name and Framework/Language](/img/guides/quickstart/create_project_react.png) -![Project Created](/img/guides/quickstart/21.png) +3. An overview of the configuration for the framework you have selected is shown -### 4. Add users (optional) +![Project Configuration Overview - React App](/img/guides/quickstart/project_config_overview_react.png) -[Skip optional steps](quickstart/#7-create-an-application-in-your-project) and jump directly to the create application step. +What the configuration does: +- We recommend using the [Authentication Code Flow](/docs/apis/openidoauth/grant-types#authorization-code) with [Proof Key for Code Exchange (PKCE)](/docs/apis/openidoauth/grant-types#authorization-code) for secure OIDC configuration. This setup includes specific App, Grant, and Response Types, as well as an Authentication Method. More about the different app types can be found [here](/docs/guides/integrate/login/oidc/oauth-recommended-flows#different-client-profiles). +- The redirect URI is where the ZITADEL authorization server redirects the user after they have been authenticated. The "Redirect URI" is preconfigured to [http://localhost:3000/callback](http://localhost:3000/callback) and the “Post Logout URI” to [http://localhost:3000/](http://localhost:3000/) for the React example + +4. The detailed configuration of your app will be shown + +![Project Name and Framework/Language](/img/guides/quickstart/app_detail_overview_react.png) + +### 5. Add users (optional) + +[Skip optional steps](quickstart/#7-create-an-application-in-your-project) and jump directly to the obtain ClientId and OIDC endpoints. 1. To add users, click on “Users” at the top menu. You will see that the user you already created is listed as a user here. Click on the “New” button to create a new user. @@ -163,7 +183,7 @@ As a user of the ZITADEL Cloud Customer Portal, you now can create multiple inst ![User Info](/img/guides/quickstart/29.png) -### 5. Add roles to your project (optional) +### 6. Add roles to your project (optional) 1. To add roles to your project, click on “Roles” on the left as shown below. Next, click on the “New” button. @@ -181,7 +201,7 @@ As a user of the ZITADEL Cloud Customer Portal, you now can create multiple inst ![Add Role](/img/guides/quickstart/25.png) -### 6. Add authorizations to your project (optional) +### 7. Add authorizations to your project (optional) 1. Click on “Authorizations” on the top menu. @@ -211,54 +231,6 @@ As a user of the ZITADEL Cloud Customer Portal, you now can create multiple inst ![Add Authorization](/img/guides/quickstart/37.png) -### 7. Create an application in your project - -We will now create an application in the project, which will allow our React client application to access protected resources through the use of the OpenID Connect (OIDC) protocol. - -1. Go to “Projects” and click on “Project1”. - -![Add Application](/img/guides/quickstart/38.png) - -2. Click on “New” to add a new application. - -![Add Application](/img/guides/quickstart/39.png) - -3. Provide the name of the app as “React-SPA”. Select “User Agent” and click on “Continue”. - -![Add Application](/img/guides/quickstart/41.png) - -4. Select “PKCE” because we recommend the use of [Authorization Code](/docs/apis/openidoauth/grant-types#authorization-code) in combination with [Proof Key for Code Exchange (PKCE)](/docs/apis/openidoauth/grant-types#authorization-code) for all web applications. More about the different app types can be found [here](/docs/guides/integrate/login/oidc/oauth-recommended-flows#different-client-profiles). Click on "Continue". - -![Add Application](/img/guides/quickstart/42.png) - -5. The Redirect URL in your application is where the ZITADEL authorization server redirects the user after they have been authenticated. Set your “Redirect URI” to [http://localhost:3000/callback](http://localhost:3000/callback) and “Post Logout URI” to [http://localhost:3000/](http://localhost:3000/). Click on “Continue”. - -![Add Application](/img/guides/quickstart/43.png) - -6. Now you will be presented with the details of your application for review. Click on “Create”. - -![Add Application](/img/guides/quickstart/44.png) - -7. After that, a window will appear showing you your “ClientId”. Click on “Close”. - -![Add Application](/img/guides/quickstart/45.png) - -8. On the following screen, you may notice some warnings at the top. These warnings are displayed because the URLs for our application are using unsecure HTTP instead of HTTPS. To resolve this, we will need to update our URLs, but there is a workaround to this so that we can continue using HTTP during the development phase. - -![Add Application](/img/guides/quickstart/46.png) - -9. To proceed with HTTP, go to “Redirect Settings” on the left. - -![Add Application](/img/guides/quickstart/47.png) - -10. Now activate “Development Mode” as shown below and click “Save”. - -![Add Application](/img/guides/quickstart/48.png) - -11. Now if you go to “Projects” and “Project1”, you will see the “React-SPA” app listed as an application in the project: - -![Add Application](/img/guides/quickstart/50.png) - ### 8. Obtain ClientId and OIDC endpoints for your application {#referred1} You will need the ClientId and the OIDC endpoints (issuer and userinfo) when building your React application. The issuer URL is the base URL for the OIDC provider and includes the path to the OIDC discovery document, which contains information about the OIDC provider, including the authorization and token endpoints. By providing the issuer URL, you can use the OIDC library to automatically determine the endpoints for these requests. diff --git a/docs/docs/legal/policies/rate-limit-policy.md b/docs/docs/legal/policies/rate-limit-policy.md index 6075b7e6ab..b23f822c0f 100644 --- a/docs/docs/legal/policies/rate-limit-policy.md +++ b/docs/docs/legal/policies/rate-limit-policy.md @@ -3,7 +3,7 @@ title: Rate Limit Policy custom_edit_url: null --- -Last updated on April 24, 2024 +Last updated on February 24, 2025 This policy is an annex to the [Terms of Service](../terms-of-service) and clarifies your obligations while using our Services, specifically how we will use rate limiting to enforce certain aspects of our [Acceptable Use Policy](acceptable-use-policy). @@ -15,7 +15,7 @@ To ensure the availability of our Services and to avoid slow or failed requests ZITADEL Clouds rate limit is built around a `IP` oriented model. Please be aware that we also utilize a service for DDoS mitigation. -So if you simply change your `IP` address and run the same request again and again you might be get blocked at some point. +So if you simply change your `IP` address and run the same request again and again you might get blocked at some point. If you are blocked you will receive a `http status 429`. @@ -26,21 +26,19 @@ You should consider to implement [exponential backoff](https://en.wikipedia.org/ :::info Raising limits We understand that there are certain scenarios where your users access ZITADEL from shared IP Addresses. For example if you use a corporate proxy or Network Address Translation NAT. -Please [get in touch](https://zitadel.com/contact) with us to discuss your requirements and we'll find a solution. +Please [get in touch](https://zitadel.com/contact) with us to discuss your requirements, and we'll find a solution. ::: ## What rate limits do apply -For ZITADEL Cloud, we have a rate limiting rule for login paths (login, register and reset features) and for API paths each. +For ZITADEL Cloud, we have dedicated rate limits for the user interfaces (login, register, console,...) and the APIs. Rate limits are implemented with the following rules: -| Path | Description | Rate Limiting | One Minute Banning | -| -------------------- | -------------------------------------------------------------- | ------------------------------------ | ------------------------------------- | -| /ui/login\* | Global Login, Register and Reset Limit | 10 requests per second over a minute | 15 requests per second over 3 minutes | -| /oauth/v2/keys | OAuth/OpenID Public Keys Endpoint | 20 requests per second over a minute | 15 requests per second over 3 minutes | -| /oauth/v2/introspect | OAuth Introspection Endpoint | 20 requests per second over a minute | 15 requests per second over 3 minutes | -| All other paths | All gRPC- and REST APIs as well as the ZITADEL Customer Portal | 10 requests per second over a minute | 10 requests per second over 3 minutes | +| Path | Description | Rate Limiting | One Minute Banning | +|----------------------|----------------------------------------|--------------------------------------|---------------------------------------| +| /ui/\* | Global Login, Register and Reset Limit | 10 requests per second over a minute | 15 requests per second over 3 minutes | +| All other paths | All gRPC-, REST and OAuth APIs | 50 requests per second over a minute | 50 requests per second over 3 minutes | ## Load Testing diff --git a/docs/docs/sdk-examples/introduction.mdx b/docs/docs/sdk-examples/introduction.mdx index de928731c3..203e21e9f3 100644 --- a/docs/docs/sdk-examples/introduction.mdx +++ b/docs/docs/sdk-examples/introduction.mdx @@ -9,6 +9,14 @@ To achieve your goals as fast as possible, we provide you with SDKs, Example Rep The SDKs and integration depend on the framework and language you are using. +:::note +In addition to our officially maintained examples, we also list community-contributed implementations. +These examples are provided by external developers and are not maintained by us. +While we believe they can be valuable resources and showcase diverse approaches, we cannot guarantee their completeness, functionality, or continued support. +If you encounter issues with a community-contributed example, please contact the respective maintainers directly. +We provide this list for informational purposes and to foster community engagement, but we do not assume responsibility for these external implementations. +::: + import { Frameworks } from "../../src/components/frameworks"; ## Resources diff --git a/docs/docs/sdk-examples/java.mdx b/docs/docs/sdk-examples/java.mdx index e7afb5188b..e4862422d0 100644 --- a/docs/docs/sdk-examples/java.mdx +++ b/docs/docs/sdk-examples/java.mdx @@ -41,7 +41,7 @@ The following features are covered by Java Spring Security: The goal is to have a ZITADEL Java SDK in the future which will cover the following: - Wrapper around Java Spring Security - Authentication with OIDC -- Authorization and checking Rolls +- Authorization and checking Roles - Integrate ZITADEL APIs to read and manage resources - Integrate ZITADEL Session API to create your own login UI diff --git a/docs/docs/self-hosting/deploy/docker-compose-sa.yaml b/docs/docs/self-hosting/deploy/docker-compose-sa.yaml index 95608fd76d..9edd95faa0 100644 --- a/docs/docs/self-hosting/deploy/docker-compose-sa.yaml +++ b/docs/docs/self-hosting/deploy/docker-compose-sa.yaml @@ -32,7 +32,7 @@ services: db: restart: 'always' - image: postgres:16-alpine + image: postgres:17-alpine environment: PGUSER: postgres POSTGRES_PASSWORD: postgres diff --git a/docs/docs/self-hosting/deploy/docker-compose.yaml b/docs/docs/self-hosting/deploy/docker-compose.yaml index e32700ace4..f5164eb3b7 100644 --- a/docs/docs/self-hosting/deploy/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/docker-compose.yaml @@ -24,7 +24,7 @@ services: db: restart: 'always' - image: postgres:16-alpine + image: postgres:17-alpine environment: PGUSER: postgres POSTGRES_PASSWORD: postgres diff --git a/docs/docs/self-hosting/deploy/knative.mdx b/docs/docs/self-hosting/deploy/knative.mdx index b26c7189bd..0c8e7f0a36 100644 --- a/docs/docs/self-hosting/deploy/knative.mdx +++ b/docs/docs/self-hosting/deploy/knative.mdx @@ -13,34 +13,39 @@ import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx Follow the [Knative quickstart guide](https://knative.dev/docs/getting-started/quickstart-install/) to get a local kind/minikube environment with Knative capabilities. -## Run CockroachDB +For example, to install Knative on a kind cluster, run `kn quickstart kind`. -Start a single-node cockroachdb as statefulset +## Run PostgreSQL + +If you are following the Knative Tutorial, you can deploy Postgres as a StatefulSet for the tutorials Bookstore sample app. For example: ```bash -kubectl apply -f https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/knative/cockroachdb-statefulset-single-node.yaml +git clone https://github.com/knative/docs.git +kubectl apply -f docs/code-samples/eventing/bookstore-sample-app/solution/db-service/ ``` -## Start ZITADEL - -### Knative Command +## Start Zitadel as a Knative Service ```bash kn service create zitadel \ --image ghcr.io/zitadel/zitadel:latest \ --port 8080 \ ---env ZITADEL_DATABASE_COCKROACH_HOST=cockroachdb \ +--env ZITADEL_EXTERNALDOMAIN=zitadel.default.127.0.0.1.sslip.io \ --env ZITADEL_EXTERNALSECURE=false \ --env ZITADEL_EXTERNALPORT=80 \ --env ZITADEL_TLS_ENABLED=false \ ---env ZITADEL_EXTERNALDOMAIN=zitadel.default.127.0.0.1.sslip.io \ ---arg "start-from-init" --arg "--masterkey" --arg "MasterkeyNeedsToHave32Characters" -``` - -### Knavite yaml - -```bash -kubectl apply -f https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/knative/zitadel-knative-service.yaml +--env ZITADEL_DATABASE_POSTGRES_HOST=postgresql \ +--env ZITADEL_DATABASE_POSTGRES_PORT=5432 \ +--env ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel \ +--env ZITADEL_DATABASE_POSTGRES_USER_USERNAME=myzitadeluser \ +--env ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=myzitadelpassword \ +--env ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable \ +--env ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=myuser \ +--env ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=mypassword \ +--env ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable \ +--env ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED=false \ +--env ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MFAINITSKIPLIFETIME="0s" \ +--arg "start-from-init" --arg "--masterkey=MasterkeyNeedsToHave32Characters" ``` ## Access ZITADEL @@ -54,17 +59,9 @@ NAME URL LATEST AGE COND zitadel http://zitadel.default.127.0.0.1.sslip.io zitadel-00001 10m 3 OK / 3 True ``` -Add the console path to the URL and open in browser -http://zitadel.default.127.0.0.1.sslip.io/ui/console - -If you didn't configure something else, this is the default IAM admin users login: - -* username: zitadel-admin@zitadel.zitadel.default.127.0.0.1.sslip.io -* password: Password1! +Open your browser at http://zitadel.default.127.0.0.1.sslip.io/ui/console?login_hint=zitadel-admin@zitadel.zitadel.default.127.0.0.1.sslip.io and use the initial password _Password1!_ -## VideoGuide - diff --git a/docs/docs/self-hosting/deploy/linux.mdx b/docs/docs/self-hosting/deploy/linux.mdx index eb7f4dc90d..90774e97ab 100644 --- a/docs/docs/self-hosting/deploy/linux.mdx +++ b/docs/docs/self-hosting/deploy/linux.mdx @@ -1,5 +1,5 @@ --- -title: Install ZITADEL on Linux +title: Install Zitadel on Linux sidebar_label: Linux --- @@ -11,7 +11,7 @@ import NoteInstanceNotFound from "./troubleshooting/_note_instance_not_found.mdx ## Install PostgreSQL Download a `postgresql` binary as described [in the PostgreSQL docs](https://www.postgresql.org/download/linux/). -ZITADEL is tested against PostgreSQL and CockroachDB latest stable tag and Ubuntu 22.04. +Zitadel is tested against PostgreSQL latest stable tag and latest Ubuntu LTS. ## Run PostgreSQL @@ -20,15 +20,15 @@ sudo systemctl start postgresql sudo systemctl enable postgresql ``` -## Install ZITADEL +## Install Zitadel -Download the ZITADEL release according to your architecture from [Github](https://github.com/zitadel/zitadel/releases/latest), unpack the archive and copy zitadel binary to /usr/local/bin +Download the Zitadel release according to your architecture from [Github](https://github.com/zitadel/zitadel/releases/latest), unpack the archive and copy zitadel binary to /usr/local/bin ```bash LATEST=$(curl -i https://github.com/zitadel/zitadel/releases/latest | grep location: | cut -d '/' -f 8 | tr -d '\r'); ARCH=$(uname -m); case $ARCH in armv5*) ARCH="armv5";; armv6*) ARCH="armv6";; armv7*) ARCH="arm";; aarch64) ARCH="arm64";; x86) ARCH="386";; x86_64) ARCH="amd64";; i686) ARCH="386";; i386) ARCH="386";; esac; wget -c https://github.com/zitadel/zitadel/releases/download/$LATEST/zitadel-linux-$ARCH.tar.gz -O - | tar -xz && sudo mv zitadel-linux-$ARCH/zitadel /usr/local/bin ``` -## Run ZITADEL +## Run Zitadel ```bash ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled @@ -50,7 +50,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT allowfullscreen > -### Setup ZITADEL with a service account +### Setup Zitadel with a service account ```bash ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH=/tmp/zitadel-admin-sa.json ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE=1 zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml index 94d8f438dc..d1d8c95bb2 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml @@ -25,7 +25,7 @@ services: - './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro' db: - image: postgres:16-alpine + image: postgres:17-alpine restart: always environment: - POSTGRES_USER=root diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx index 7f25a9b210..d5e3984568 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx @@ -81,5 +81,5 @@ Read more about [the login process](/guides/integrate/login/oidc/login-users). ## Troubleshooting -You can connect to cockroach like this: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --host my-cockroach-db --certs-dir /cockroach/certs/` -For example, to show all login names: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --database zitadel --host my-cockroach-db --certs-dir /cockroach/certs/ --execute "select * from projections.login_names3"` +You can connect to the database like this: `docker exec -it loadbalancing-example-db-1 psql --host localhost` +For example, to show all login names: `docker exec -it loadbalancing-example-db-1 psql -d zitadel --host localhost -c 'select * from projections.login_names3'` diff --git a/docs/docs/self-hosting/deploy/macos.mdx b/docs/docs/self-hosting/deploy/macos.mdx index f736255478..beb3182208 100644 --- a/docs/docs/self-hosting/deploy/macos.mdx +++ b/docs/docs/self-hosting/deploy/macos.mdx @@ -11,7 +11,7 @@ import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx ## Install PostgreSQL Download a `postgresql` binary as described [in the PostgreSQL docs](https://www.postgresql.org/download/macosx/). -ZITADEL is tested against PostgreSQL and CockroachDB latest stable tag and Ubuntu 22.04. +ZITADEL is tested against PostgreSQL latest stable tag and latest Ubuntu LTS. ## Run PostgreSQL diff --git a/docs/docs/self-hosting/deploy/overview.mdx b/docs/docs/self-hosting/deploy/overview.mdx index 38517c52f4..68255d4ce3 100644 --- a/docs/docs/self-hosting/deploy/overview.mdx +++ b/docs/docs/self-hosting/deploy/overview.mdx @@ -14,7 +14,7 @@ Choose your platform and run ZITADEL with the most minimal configuration possibl ## Prerequisites - For test environments, ZITADEL does not need many resources, 1 CPU and 512MB memory are more than enough. (With more CPU, the password hashing might be faster) -- A PostgreSQL or CockroachDB as only needed storage. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use Postgresql. +- A PostgreSQL as only needed storage. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide to use Postgresql. ## Releases diff --git a/docs/docs/self-hosting/manage/cache.md b/docs/docs/self-hosting/manage/cache.md index 30619ad283..32973d5586 100644 --- a/docs/docs/self-hosting/manage/cache.md +++ b/docs/docs/self-hosting/manage/cache.md @@ -110,7 +110,6 @@ Drawbacks: - Slowest of the available caching options - Might put additional strain on the database server, limiting horizontal scalability -- CockroachDB does not support unlogged tables. When this connector is enabled against CockroachDB, it does work but little to no performance benefit is to be expected. ### Local memory cache diff --git a/docs/docs/self-hosting/manage/cli/mirror.mdx b/docs/docs/self-hosting/manage/cli/mirror.mdx index 1c32dc8741..45bac9b279 100644 --- a/docs/docs/self-hosting/manage/cli/mirror.mdx +++ b/docs/docs/self-hosting/manage/cli/mirror.mdx @@ -1,5 +1,5 @@ --- -title: Mirror data to another database +title: Mirror data from cockroach to postgres sidebar_label: Mirror command --- @@ -9,15 +9,15 @@ The data can be mirrored to multiple database without influencing each other. ## Use cases -Migrate from cockroachdb to postgres or vice versa. +Migrate from cockroachdb to postgres. Replicate data to a secondary environment for testing. ## Prerequisites -You need an existing source database, most probably the database ZITADEL currently serves traffic from. +You need an existing source database, most probably the database Zitadel currently serves traffic from. -To mirror the data the destination database needs to be initialized and setup without an instance. +To mirror the data, the destination database needs to be initialized and set up without an instance. You can find the commands to start an empty Zitadel deployment in [the example section](#prepare-the-destination-database). ### Start the destination database @@ -38,14 +38,32 @@ docker compose up db --detach ## Example -The following commands setup the database as described above. See [configuration](#configuration) for more details about the configuration options. +### Prepare the destination database + +The following commands setup the database without an instance. ```bash zitadel init --config /path/to/your/new/config.yaml zitadel setup --for-mirror --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +``` + +### Mirror the data + +The next step is to copy the data from the source to the destination database. For detailed configuration options, please refer to the [configuration section](#configuration). + +```bash zitadel mirror --system --config /path/to/your/mirror/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment ``` +### Initialize the data and verify + +The last step is to setup the permissions and verify the data, there might be differences between source and destination, refer to [`zitadel mirror verify`](#zitadel-mirror-verify) to get an overview of possible diffs. + +```bash +zitadel setup --for-mirror --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +zitadel mirror verify --system --config /path/to/your/mirror/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment +``` + ## Usage The general syntax for the mirror command is: @@ -73,7 +91,7 @@ Flags: --masterkey string masterkey as argument for en/decryption keys -m, --masterkeyFile string path to the masterkey for en/decryption keys --masterkeyFromEnv read masterkey for en/decryption keys from environment variable (ZITADEL_MASTERKEY) - --tlsMode externalSecure start ZITADEL with (enabled), without (disabled) TLS or external component e.g. reverse proxy (external) terminating TLS, this flag will overwrite externalSecure and `tls.enabled` in configs files + --tlsMode externalSecure start Zitadel with (enabled), without (disabled) TLS or external component e.g. reverse proxy (external) terminating TLS, this flag will overwrite externalSecure and `tls.enabled` in configs files ``` ## Configuration @@ -87,8 +105,6 @@ Source: Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS - EventPushConnRatio: 0.33 # ZITADEL_SOURCE_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.33 # ZITADEL_SOURCE_COCKROACH_PROJECTIONSPOOLERCONNRATIO MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS @@ -122,44 +138,23 @@ Source: # The destination database the data are copied to. Use either cockroach or postgres, by default cockroach is used Destination: - cockroach: - Host: localhost # ZITADEL_DESTINATION_COCKROACH_HOST - Port: 26257 # ZITADEL_DESTINATION_COCKROACH_PORT - Database: zitadel # ZITADEL_DESTINATION_COCKROACH_DATABASE - MaxOpenConns: 0 # ZITADEL_DESTINATION_COCKROACH_MAXOPENCONNS - MaxIdleConns: 0 # ZITADEL_DESTINATION_COCKROACH_MAXIDLECONNS - MaxConnLifetime: 30m # ZITADEL_DESTINATION_COCKROACH_MAXCONNLIFETIME - MaxConnIdleTime: 5m # ZITADEL_DESTINATION_COCKROACH_MAXCONNIDLETIME - EventPushConnRatio: 0.01 # ZITADEL_DESTINATION_COCKROACH_EVENTPUSHCONNRATIO - ProjectionSpoolerConnRatio: 0.5 # ZITADEL_DESTINATION_COCKROACH_PROJECTIONSPOOLERCONNRATIO - Options: "" # ZITADEL_DESTINATION_COCKROACH_OPTIONS - User: - Username: zitadel # ZITADEL_DESTINATION_COCKROACH_USER_USERNAME - Password: "" # ZITADEL_DESTINATION_COCKROACH_USER_PASSWORD - SSL: - Mode: disable # ZITADEL_DESTINATION_COCKROACH_USER_SSL_MODE - RootCert: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_ROOTCERT - Cert: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_CERT - Key: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_KEY - # Postgres is used as soon as a value is set - # The values describe the possible fields to set values postgres: - Host: # ZITADEL_DESTINATION_POSTGRES_HOST - Port: # ZITADEL_DESTINATION_POSTGRES_PORT - Database: # ZITADEL_DESTINATION_POSTGRES_DATABASE - MaxOpenConns: # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS - MaxIdleConns: # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS - MaxConnLifetime: # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME - MaxConnIdleTime: # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME - Options: # ZITADEL_DESTINATION_POSTGRES_OPTIONS + Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST + Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT + Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE + MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS + MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS + MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME + MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME + Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS User: - Username: # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME - Password: # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD + Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME + Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD SSL: - Mode: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_MODE - RootCert: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_ROOTCERT - Cert: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_CERT - Key: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_KEY + Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE + RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT + Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT + Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY # As cockroachdb first copies the data into memory this parameter is used to iterate through the events table and fetch only the given amount of events per iteration EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE @@ -227,6 +222,6 @@ It is not possible to use files as source or destination. See github issue [here Currently the encryption keys of the source database must be copied to the destination database. See github issue [here](https://github.com/zitadel/zitadel/issues/7964) -It is not possible to change the domain of the ZITADEL deployment. +It is not possible to change the domain of the Zitadel deployment. Once you mirrored an instance using the `--instance` flag, you have to make sure you don't mirror other preexisting instances. This means for example, you cannot mirror a few instances and then pass the `--system` flag. You have to pass all remaining instances explicitly, once you used the `--instance` flag diff --git a/docs/docs/self-hosting/manage/configure/_helm.mdx b/docs/docs/self-hosting/manage/configure/_helm.mdx index 9f03e4237a..17fb8165a6 100644 --- a/docs/docs/self-hosting/manage/configure/_helm.mdx +++ b/docs/docs/self-hosting/manage/configure/_helm.mdx @@ -1,36 +1,6 @@ -import CodeBlock from '@theme/CodeBlock'; -import ExampleZITADELValuesSource from '!!raw-loader!./example-zitadel-values.yaml' -import ExampleZITADELValuesSecretsSource from '!!raw-loader!./example-zitadel-values-secrets.yaml' - -By default, the chart installs a secure ZITADEL and CockroachDB. -The example files makes an insecure ZITADEL accessible by port forwarding the ZITADEL service to localhost. -For more configuration options, [go to the chart repo descriptions](https://github.com/zitadel/zitadel-charts). -For a secure installation with Docker Compose, [go to the loadbalancing example](/self-hosting/deploy/loadbalancing-example) - -By executing the commands below, you will download the following files: - -
- example-zitadel-values.yaml - {ExampleZITADELValuesSource} -
- -
- example-zitadel-values-secrets.yaml - {ExampleZITADELValuesSecretsSource} -
- -```bash -# Download and adjust the example configuration file containing standard configuration -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-values.yaml - -# Download and adjust the example configuration file containing secret configuration -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-values-secrets.yaml - -# Install an insecure zitadel release that works with localhost -helm install --namespace zitadel --create-namespace my-zitadel zitadel/zitadel \ - --values ./example-zitadel-values.yaml \ - --values ./example-zitadel-values-secrets.yaml - -# Forward the ZITADEL service port to your local machine -kubectl --namespace zitadel port-forward svc/my-zitadel 8080:80 -``` +To run Zitadel on Kubernetes, use [the official Zitadel Helm chart](https://github.com/zitadel/zitadel-charts). +Configure Zitadel using native Helm values. +You can manage secrets through Helm values, letting Helm create Kubernetes secrets. +Alternatively, reference existing Kubernetes secrets managed outside of Helm. +See the [referenced secrets example](https://github.com/zitadel/zitadel-charts/tree/main/examples/3-referenced-secrets) in the charts */examples* folder. +For a quick setup, check out the [insecure Postgres example](https://github.com/zitadel/zitadel-charts/tree/main/examples/1-postgres-insecure). diff --git a/docs/docs/self-hosting/manage/configure/_login.md b/docs/docs/self-hosting/manage/configure/_login.md new file mode 100644 index 0000000000..2fc258b299 --- /dev/null +++ b/docs/docs/self-hosting/manage/configure/_login.md @@ -0,0 +1 @@ +Open your favorite internet browser at http://localhost:8080/ui/console?login_hint=root@zitadel.localhost and use the password _RootPassword1!_ diff --git a/docs/docs/self-hosting/manage/configure/configure.mdx b/docs/docs/self-hosting/manage/configure/configure.mdx index aaf221dfda..c68f716d63 100644 --- a/docs/docs/self-hosting/manage/configure/configure.mdx +++ b/docs/docs/self-hosting/manage/configure/configure.mdx @@ -8,6 +8,7 @@ import TabItem from "@theme/TabItem"; import LinuxUnix from "./_linuxunix.mdx"; import Compose from "./_compose.mdx"; import Helm from "./_helm.mdx"; +import Login from "./_login.md"; import CodeBlock from "@theme/CodeBlock"; import DefaultsYamlSource from "!!raw-loader!./defaults.yaml"; import StepsYamlSource from "!!raw-loader!./steps.yaml"; @@ -90,21 +91,17 @@ There are three ways to pass the masterkey to the `zitadel` binary: > + + -Open your favorite internet browser at [http://localhost:8080/ui/console](http://localhost:8080/ui/console). -This is the IAM admin users login according to your configuration in the [example-zitadel-init-steps.yaml](./example-zitadel-init-steps.yaml): - -- **username**: _root@zitadel.localhost_ -- **password**: _RootPassword1!_ - ## What's next - Read more about [the login process](/guides/integrate/login/login-users). diff --git a/docs/docs/self-hosting/manage/configure/docker-compose.yaml b/docs/docs/self-hosting/manage/configure/docker-compose.yaml index abd1818a7b..3fd0e5471c 100644 --- a/docs/docs/self-hosting/manage/configure/docker-compose.yaml +++ b/docs/docs/self-hosting/manage/configure/docker-compose.yaml @@ -11,9 +11,12 @@ services: - "./example-zitadel-config.yaml:/example-zitadel-config.yaml:ro" - "./example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro" - "./example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro" + depends_on: + db: + condition: "service_healthy" db: - image: postgres:16-alpine + image: postgres:17-alpine restart: always environment: - POSTGRES_USER=root @@ -25,7 +28,7 @@ services: interval: 10s timeout: 60s retries: 5 - start_period: 10s + start_period: 10s volumes: - 'data:/var/lib/postgresql/data:rw' @@ -34,3 +37,4 @@ networks: volumes: data: + diff --git a/docs/docs/self-hosting/manage/configure/example-zitadel-values-secrets.yaml b/docs/docs/self-hosting/manage/configure/example-zitadel-values-secrets.yaml deleted file mode 100644 index 99e5ce5647..0000000000 --- a/docs/docs/self-hosting/manage/configure/example-zitadel-values-secrets.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml -zitadel: - - masterkey: 'MasterkeyNeedsToHave32Characters' - - secretConfig: - - Database: - postgres: - User: - # If the user doesn't exist already, it is created - Username: 'root' - Password: 'Secret_DB_User_Password' - Admin: - Username: 'root' - Password: '' diff --git a/docs/docs/self-hosting/manage/configure/example-zitadel-values.yaml b/docs/docs/self-hosting/manage/configure/example-zitadel-values.yaml deleted file mode 100644 index 571c7af699..0000000000 --- a/docs/docs/self-hosting/manage/configure/example-zitadel-values.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml -zitadel: - configmapConfig: - Log: - Level: 'info' - - # Make ZITADEL accessible over HTTP, not HTTPS - ExternalSecure: false - ExternalDomain: localhost - - # the configmap is also passed to the zitadel binary via the --steps flag - FirstInstance: - Org: - Human: - # use the loginname root@zitadel.localhost - Username: 'root' - Password: 'RootPassword1!' diff --git a/docs/docs/self-hosting/manage/database/_cockroachdb.mdx b/docs/docs/self-hosting/manage/database/_cockroachdb.mdx index edc3f139fd..90c99470e6 100644 --- a/docs/docs/self-hosting/manage/database/_cockroachdb.mdx +++ b/docs/docs/self-hosting/manage/database/_cockroachdb.mdx @@ -1,8 +1,12 @@ -## ZITADEL with Cockroach +## Zitadel v2 with Cockroach -The default database of ZITADEL is [CockroachDB](https://www.cockroachlabs.com). The SQL database provides a bunch of features like horizontal scalability, data regionality and many more. +:::warning +Zitadel v3 removed CockroachDB support. See the [CLI mirror guide](../cli/mirror) for migrating to PostgreSQL. +::: -Currently versions >= 23.2 are supported. +The default database of Zitadel v2 is [CockroachDB](https://www.cockroachlabs.com). The SQL database provides a bunch of features like horizontal scalability, data regionality and many more. + +Currently versions >= 25.1 are supported. The default configuration of the database looks like this: diff --git a/docs/docs/self-hosting/manage/database/_postgres.mdx b/docs/docs/self-hosting/manage/database/_postgres.mdx index 604d6b39a5..719fb9469e 100644 --- a/docs/docs/self-hosting/manage/database/_postgres.mdx +++ b/docs/docs/self-hosting/manage/database/_postgres.mdx @@ -1,6 +1,8 @@ ## ZITADEL with Postgres -If you want to use a PostgreSQL database you can [overwrite the default configuration](../configure/configure.mdx). +PostgreSQL is the default database for ZITADEL due to its reliability, robustness, and adherence to SQL standards. It is well-suited for handling the complex data requirements of an identity management system. + +If you are using Zitadel v2 and want to use a PostgreSQL database you can [overwrite the default configuration](../configure/configure.mdx). Currently versions >= 14 are supported. diff --git a/docs/docs/self-hosting/manage/database/database.mdx b/docs/docs/self-hosting/manage/database/database.mdx index c67ecbaaba..df491e1565 100644 --- a/docs/docs/self-hosting/manage/database/database.mdx +++ b/docs/docs/self-hosting/manage/database/database.mdx @@ -11,10 +11,10 @@ import Postgres from './_postgres.mdx' diff --git a/docs/docs/self-hosting/manage/production.md b/docs/docs/self-hosting/manage/production.md index fde620b13e..98296281ea 100644 --- a/docs/docs/self-hosting/manage/production.md +++ b/docs/docs/self-hosting/manage/production.md @@ -111,14 +111,15 @@ but in the Projections.Customizations.Telemetry section ### Prefer PostgreSQL -ZITADEL supports [CockroachDB](https://www.cockroachlabs.com/) and [PostgreSQL](https://www.postgresql.org/). -We recommend using PostgreSQL, as it is the better choice when you want to prioritize performance and latency. +ZITADEL supports [PostgreSQL](https://www.postgresql.org/). -However, if [multi-regional data locality](https://www.cockroachlabs.com/docs/stable/multiregion-overview.html) is a critical requirement, CockroachDB might be a suitable option. +:::info +ZITADEL v2 supports [CockroachDB](https://www.cockroachlabs.com/) and [PostgreSQL](https://www.postgresql.org/). Please refer to [the mirror guide](cli/mirror) to migrate to postgres. +::: The indexes for the database are optimized using load tests from [ZITADEL Cloud](https://zitadel.com), which runs with PostgreSQL. -If you identify problems with your CockroachDB during load tests that indicate that the indexes are not optimized, +If you identify problems with your database during load tests that indicate that the indexes are not optimized, please create an issue in our [github repository](https://github.com/zitadel/zitadel). ### Configure ZITADEL @@ -129,12 +130,13 @@ Depending on your environment, you maybe would want to tweak some settings about Database: postgres: Host: localhost - Port: 26257 + Port: 5432 Database: zitadel //highlight-start - MaxOpenConns: 20 + MaxOpenConns: 10 + MaxIdleConns: 5 MaxConnLifetime: 30m - MaxConnIdleTime: 30m + MaxConnIdleTime: 5m //highlight-end Options: "" ``` @@ -192,9 +194,7 @@ The ZITADEL binary itself is stateless, so there is no need for a special backup job. Generally, for maintaining your database management system in production, -please refer to the corresponding docs -[for CockroachDB](https://www.cockroachlabs.com/docs/stable/recommended-production-settings.html) -or [for PostgreSQL](https://www.postgresql.org/docs/current/admin.html). +please refer to the corresponding docs [for PostgreSQL](https://www.postgresql.org/docs/current/admin.html). ## Data initialization @@ -240,8 +240,7 @@ you might want to [limit usage and/or execute tasks on certain usage units and l ### General resource usage -ZITADEL consumes around 512MB RAM and can run with less than 1 CPU core. -The database consumes around 2 CPU under normal conditions and 6GB RAM with some caching to it. +ZITADEL itself requires approximately 512MB of RAM and can operate with less than one CPU core. The database component, under typical conditions, utilizes about one CPU core per 100 requests per second (req/s) and 4GB of RAM per core, which includes some caching. :::info Password hashing Be aware of CPU spikes when hashing passwords. We recommend to have 4 CPU cores available for this purpose. @@ -249,5 +248,6 @@ Be aware of CPU spikes when hashing passwords. We recommend to have 4 CPU cores ### Production HA cluster -It is recommended to build a minimal high-availability with 3 Nodes with 4 CPU and 16GB memory each. -Excluding non-essential services, such as log collection, metrics etc, the resources could be reduced to around 4 CPU and 8GB memory each. +For a minimal high-availability setup, we recommend a cluster of 3 nodes, each with 4 CPU cores and 16GB of memory. + +If you exclude non-essential services like log collection and metrics, you can reduce the resources to approximately 4 CPU cores and 8GB of memory per node. diff --git a/docs/docs/self-hosting/manage/productionchecklist.md b/docs/docs/self-hosting/manage/productionchecklist.md index fb85557a23..c4f6491d82 100644 --- a/docs/docs/self-hosting/manage/productionchecklist.md +++ b/docs/docs/self-hosting/manage/productionchecklist.md @@ -19,7 +19,9 @@ To apply best practices to your production setup we created a step by step check - [ ] Use serverless platform such as Knative or a hyperscaler equivalent (e.g. CloudRun from Google) - [ ] Split `zitadel init` and `zitadel setup` for fast start-up times when [scaling](/docs/self-hosting/manage/updating_scaling) ZITADEL - [ ] High Availability for database - - [ ] Follow the [Production Checklist](https://www.cockroachlabs.com/docs/stable/recommended-production-settings.html) for CockroachDB if you selfhost the database or use [CockroachDB cloud](https://www.cockroachlabs.com/docs/cockroachcloud/create-an-account.html) + - [ ] Follow [this guide](https://www.postgresql.org/docs/current/high-availability.html) to set up the database. + - [ ] Configure logging + - [ ] Configure timeouts - [ ] Configure backups on a regular basis for the database - [ ] Test the restore scenarios before going live - [ ] Secure database connections from outside your network and/or use an internal subnet for database connectivity diff --git a/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_overview.mdx b/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_overview.mdx index 3200ddf379..f72eb6eec5 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_overview.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_overview.mdx @@ -2,13 +2,13 @@ import CodeBlock from '@theme/CodeBlock'; import ComposeYaml from "!!raw-loader!./docker-compose.yaml"; <>With these examples, you create and run a minimal {props.link} configuration for ZITADEL with Docker Compose. -Whereas the guide focuses on the configuration for {props.link}, you can inspect the configurations for ZITADEL and the database in the base Docker Compose file. +Whereas the guide focuses on the configuration for {props.name}, you can inspect the configurations for ZITADEL and the database in the base Docker Compose file.
base docker-compose.yaml {ComposeYaml}
-<>For running {props.link}, you will extend the base Docker Compose file with the {props.link} specific Docker Compose file. +<>For running {props.name}, you will extend the base Docker Compose file with the {props.name} specific Docker Compose file.
specific docker-compose.yaml diff --git a/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_tls_mode.mdx b/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_tls_mode.mdx index 1cacf076e5..43663af486 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_tls_mode.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/_proxy_guide_tls_mode.mdx @@ -1,25 +1,25 @@ import CodeBlock from '@theme/CodeBlock'; -export const Description = ({mode, link}) => { +export const Description = ({mode, name}) => { let desc switch (mode) { case "disabled": - desc = <>Neither {link} nor ZITADEL terminates TLS. - Nevertheless, {link} forwards unencrypted HTTP/2 traffic, aka h2c, to ZITADEL.; + desc = <>Neither {name} nor ZITADEL terminates TLS. + Nevertheless, {name} forwards unencrypted HTTP/2 traffic, aka h2c, to ZITADEL.; break; case "external": - desc = <>{link} terminates TLS and forwards the requests to ZITADEL via unencrypted h2c. - This example uses an unsafe self-signed certificate for {link}; + desc = <>{name} terminates TLS and forwards the requests to ZITADEL via unencrypted h2c. + This example uses an unsafe self-signed certificate for {name}; break; case "enabled": - desc = <>{link} terminates TLS and forwards the requests to ZITADEL via encrypted HTTP/2. - This example uses an unsafe self-signed certificate for {link} and the same for ZITADEL.; + desc = <>{name} terminates TLS and forwards the requests to ZITADEL via encrypted HTTP/2. + This example uses an unsafe self-signed certificate for {name} and the same for ZITADEL.; break; } return ( <> {desc} - <>By executing the commands below, you will download the files necessary to run ZITADEL behind {link} with the following config: + <>By executing the commands below, you will download the files necessary to run ZITADEL behind {name} with the following config: ) } diff --git a/docs/docs/self-hosting/manage/reverseproxy/caddy/caddy.mdx b/docs/docs/self-hosting/manage/reverseproxy/caddy/caddy.mdx index 20a00dae9b..5fb9ea4014 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/caddy/caddy.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/caddy/caddy.mdx @@ -15,7 +15,7 @@ export const providername = 'Caddy'; export const lower = "caddy"; export const link = {providername} - + You can either setup your environment for TLS mode external or TLS mode enabled. diff --git a/docs/docs/self-hosting/manage/reverseproxy/docker-compose.yaml b/docs/docs/self-hosting/manage/reverseproxy/docker-compose.yaml index 989b620fef..c4a4f93fb2 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/docker-compose.yaml +++ b/docs/docs/self-hosting/manage/reverseproxy/docker-compose.yaml @@ -121,7 +121,7 @@ services: db: restart: 'always' - image: postgres:16-alpine + image: postgres:17-alpine environment: POSTGRES_PASSWORD: postgres healthcheck: diff --git a/docs/docs/self-hosting/manage/reverseproxy/httpd/httpd.mdx b/docs/docs/self-hosting/manage/reverseproxy/httpd/httpd.mdx index 4d75802ec4..c869155d05 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/httpd/httpd.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/httpd/httpd.mdx @@ -15,7 +15,7 @@ export const providername = "Apache httpd"; export const lower = "httpd"; export const link = {providername} - + You can either setup your environment for TLS mode disabled, TLS mode external or TLS mode enabled. diff --git a/docs/docs/self-hosting/manage/reverseproxy/nginx/nginx.mdx b/docs/docs/self-hosting/manage/reverseproxy/nginx/nginx.mdx index 0ad5c036b7..fa3a9e75de 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/nginx/nginx.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/nginx/nginx.mdx @@ -15,7 +15,7 @@ export const providername = 'NGINX'; export const lower = "nginx"; export const link = {providername}; - + You can either setup your environment for TLS mode disabled, TLS mode external or TLS mode enabled. diff --git a/docs/docs/self-hosting/manage/reverseproxy/traefik/traefik.mdx b/docs/docs/self-hosting/manage/reverseproxy/traefik/traefik.mdx index d5950ad93c..39769b229b 100644 --- a/docs/docs/self-hosting/manage/reverseproxy/traefik/traefik.mdx +++ b/docs/docs/self-hosting/manage/reverseproxy/traefik/traefik.mdx @@ -15,7 +15,7 @@ export const providername = 'Traefik'; export const lower = "traefik"; export const link = {providername}; - + You can either setup your environment for TLS mode disabled, TLS mode external or TLS mode enabled. diff --git a/docs/docs/self-hosting/manage/updating_scaling.md b/docs/docs/self-hosting/manage/updating_scaling.md index 7b8c72bd32..046c8891d0 100644 --- a/docs/docs/self-hosting/manage/updating_scaling.md +++ b/docs/docs/self-hosting/manage/updating_scaling.md @@ -53,10 +53,10 @@ The command `zitadel init` ensures the database connection is ready to use for t It just needs to be executed once over ZITADELs full life cycle, when you install ZITADEL from scratch. During `zitadel init`, for connecting to your database, -ZITADEL uses the privileged and preexisting database user configured in `Database..Admin.Username`. +ZITADEL uses the privileged and preexisting database user configured in `Database.postgres.Admin.Username`. , `zitadel init` ensures the following: - If it doesn’t exist already, it creates a database with the configured database name. -- If it doesn’t exist already, it creates the unprivileged user use configured in `Database..User.Username`. +- If it doesn’t exist already, it creates the unprivileged user use configured in `Database.postgres.User.Username`. Subsequent phases connect to the database with this user's credentials only. - If not already done, it grants the necessary permissions ZITADEL needs to the non privileged user. - If they don’t exist already, it creates all schemas and some basic tables. diff --git a/docs/docs/support/advisory/a10015.md b/docs/docs/support/advisory/a10015.md new file mode 100644 index 0000000000..0945f40361 --- /dev/null +++ b/docs/docs/support/advisory/a10015.md @@ -0,0 +1,21 @@ +--- +title: Technical Advisory 10015 +--- + +## Date + +Versions: >= v3.0.0 + +Date: 2025-03-31 + +## Description + +CockroachDB was initially chosen due to its distributed nature and SQL compatibility. However, over time, it became apparent that the operational complexity and specific compatibility issues outweighed the benefits for our use case. We decided to focus on PostgreSQL to simplify our infrastructure and leverage its mature ecosystem. + +## Impact + +Zitadel v3 requires PostgreSQL as a database. Therefore, Zitadel v3 will not start if CockroachDB is configured as the database. + +## Mitigation + +To upgrade your self-hosted deployment to Zitadel v3 migrate to PostgreSQL. Please refer to [this guide](/docs/self-hosting/manage/cli/mirror) to mirror the data to PostgreSQL before you deploy Zitadel v3. \ No newline at end of file diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index 8805e2e1d8..0d8818c32c 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -226,6 +226,18 @@ We understand that these advisories may include breaking changes, and we aim to - 2025-01-10 + + + A-10015 + + Drop CockroachDB support + Breaking Behavior Change + + CockroachDB is no longer supported by Zitadel. + + 3.0.0 + 2025-03-31 + ## Subscribe to our Mailing List diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index d2a3be4c01..1f45a017ac 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -324,33 +324,17 @@ module.exports = { categoryLinkSource: "auto", }, }, - user_schema_v3: { - specPath: ".artifacts/openapi/zitadel/resources/userschema/v3alpha/user_schema_service.swagger.json", - outputDir: "docs/apis/resources/user_schema_service_v3", - sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", - }, - }, - user_v3: { - specPath: ".artifacts/openapi/zitadel/resources/user/v3alpha/user_service.swagger.json", - outputDir: "docs/apis/resources/user_service_v3", - sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", - }, - }, - action_v3: { - specPath: ".artifacts/openapi/zitadel/resources/action/v3alpha/action_service.swagger.json", - outputDir: "docs/apis/resources/action_service_v3", + action_v2: { + specPath: ".artifacts/openapi/zitadel/action/v2beta/action_service.swagger.json", + outputDir: "docs/apis/resources/action_service_v2", sidebarOptions: { groupPathsBy: "tag", categoryLinkSource: "auto", }, }, - webkey_v3: { - specPath: ".artifacts/openapi/zitadel/resources/webkey/v3alpha/webkey_service.swagger.json", - outputDir: "docs/apis/resources/webkey_service_v3", + webkey_v2: { + specPath: ".artifacts/openapi/zitadel/webkey/v2beta/webkey_service.swagger.json", + outputDir: "docs/apis/resources/webkey_service_v2", sidebarOptions: { groupPathsBy: "tag", categoryLinkSource: "auto", diff --git a/docs/frameworks.json b/docs/frameworks.json index 97afa11d47..163b493274 100644 --- a/docs/frameworks.json +++ b/docs/frameworks.json @@ -80,6 +80,12 @@ "docsLink": "https://github.com/maennchen/zitadel_api", "external": true }, + { + "title": "FastAPI", + "imgSrcDark": "/docs/img/tech/fastapi.svg", + "docsLink": "https://github.com/cleanenergyexchange/fastapi-zitadel-auth", + "external": true + }, { "title": "NextAuth", "imgSrcDark": "/docs/img/tech/nextjs.svg", diff --git a/docs/sidebars.js b/docs/sidebars.js index a154514d5a..92c7a00b2d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -1,3 +1,19 @@ + +const sidebar_api_auth = require("./docs/apis/resources/auth/sidebar.ts").default +const sidebar_api_mgmt = require("./docs/apis/resources/mgmt/sidebar.ts").default +const sidebar_api_admin = require("./docs/apis/resources/admin/sidebar.ts").default +const sidebar_api_system = require("./docs/apis/resources/system/sidebar.ts").default + +const sidebar_api_user_service_v2 = require("./docs/apis/resources/user_service_v2/sidebar.ts").default +const sidebar_api_session_service_v2 = require("./docs/apis/resources/session_service_v2/sidebar.ts").default +const sidebar_api_oidc_service_v2 = require("./docs/apis/resources/oidc_service_v2/sidebar.ts").default +const sidebar_api_settings_service_v2 = require("./docs/apis/resources/settings_service_v2/sidebar.ts").default +const sidebar_api_feature_service_v2 = require("./docs/apis/resources/feature_service_v2/sidebar.ts").default +const sidebar_api_org_service_v2 = require("./docs/apis/resources/org_service_v2/sidebar.ts").default +const sidebar_api_idp_service_v2 = require("./docs/apis/resources/idp_service_v2/sidebar.ts").default +const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/sidebar.ts").default +const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_service_v2/sidebar.ts").default + module.exports = { guides: [ "guides/overview", @@ -5,7 +21,7 @@ module.exports = { type: "category", label: "Get Started", collapsed: false, - link: {type: "doc", id: "guides/start/quickstart"}, + link: { type: "doc", id: "guides/start/quickstart" }, items: [ "guides/start/quickstart", { @@ -52,11 +68,11 @@ module.exports = { { type: "category", label: "Examples & SDKs", - link: {type: "doc", id: "sdk-examples/introduction"}, + link: { type: "doc", id: "sdk-examples/introduction" }, items: [ { - type: "autogenerated", - dirName: "sdk-examples" + type: "autogenerated", + dirName: "sdk-examples", }, { type: "link", @@ -68,6 +84,11 @@ module.exports = { label: "Elixir", href: "https://github.com/maennchen/zitadel_api", }, + { + type: "link", + label: "FastAPI", + href: "https://github.com/cleanenergyexchange/fastapi-zitadel-auth", + }, { type: "link", label: "NextAuth", @@ -118,6 +139,7 @@ module.exports = { items: [ "guides/manage/cloud/start", "guides/manage/cloud/instances", + "guides/manage/cloud/settings", "guides/manage/cloud/billing", "guides/manage/cloud/users", "guides/manage/cloud/support", @@ -210,22 +232,22 @@ module.exports = { { type: "link", href: "/docs/guides/integrate/login/login-users#zitadels-session-api", - label: "Session API" + label: "Session API", }, { type: "category", label: "Hosted Login", link: { type: "doc", - id: "guides/integrate/login/hosted-login" + id: "guides/integrate/login/hosted-login", }, items: [ { type: "link", href: "/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta", - label: "Login V2 [Beta]" + label: "Login V2 [Beta]", }, - ] + ], }, { type: "link", @@ -249,7 +271,6 @@ module.exports = { "guides/integrate/login/oidc/device-authorization", "guides/integrate/login/oidc/logout", "guides/integrate/login/oidc/webkeys", - ], }, "guides/integrate/login/saml", @@ -328,6 +349,7 @@ module.exports = { "guides/integrate/login-ui/logout", "guides/integrate/login-ui/oidc-standard", "guides/integrate/login-ui/saml-standard", + "guides/integrate/login-ui/device-auth", "guides/integrate/login-ui/typescript-repo", ], }, @@ -449,6 +471,60 @@ module.exports = { ], }, "guides/integrate/external-audit-log", + { + type: "category", + label: "Actions", + link: { + type: "generated-index", + title: "Use Actions to integrate ZITADEL with your Favorite Services", + slug: "/guides/integrate/actions", + description: + "With the guides in this section you will learn how to use action to integrate Zitadel with your services.", + }, + collapsed: true, + items: [ + { + type: "doc", + id: "guides/integrate/actions/usage", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-request", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-request-manipulation", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-request-signature", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-response", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-response-manipulation", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-function", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-function-manipulation", + }, + { + type: "doc", + id: "guides/integrate/actions/testing-event", + }, + { + type: "doc", + id: "guides/integrate/actions/migrate-from-v1", + }, + ], + }, ], }, { @@ -570,7 +646,7 @@ module.exports = { items: [ { type: "category", - label: "V1 (Generally Available)", + label: "V1", collapsed: false, link: { type: "generated-index", @@ -590,7 +666,7 @@ module.exports = { description: "The authentication API (aka Auth API) is used for all operations on the currently logged in user. The user id is taken from the sub claim in the token.", }, - items: require("./docs/apis/resources/auth/sidebar.ts"), + items: sidebar_api_auth, }, { type: "category", @@ -602,7 +678,7 @@ module.exports = { description: "The management API is as the name states the interface where systems can mutate IAM objects like, organizations, projects, clients, users and so on if they have the necessary access rights. To identify the current organization you can send a header x-zitadel-orgid or if no header is set, the organization of the authenticated user is set.", }, - items: require("./docs/apis/resources/mgmt/sidebar.ts"), + items: sidebar_api_mgmt, }, { type: "category", @@ -614,7 +690,7 @@ module.exports = { description: "This API is intended to configure and manage one ZITADEL instance itself.", }, - items: require("./docs/apis/resources/admin/sidebar.ts"), + items: sidebar_api_admin, }, { type: "category", @@ -628,13 +704,13 @@ module.exports = { "\n" + "Checkout the guide how to access the ZITADEL System API.", }, - items: require("./docs/apis/resources/system/sidebar.ts"), + items: sidebar_api_system, }, ], }, { type: "category", - label: "V2 (Generally Available)", + label: "V2", collapsed: false, link: { type: "doc", @@ -643,164 +719,126 @@ module.exports = { items: [ { type: "category", - label: "User Lifecycle", + label: "User", link: { type: "generated-index", title: "User Service API", slug: "/apis/resources/user_service_v2", description: - "This API is intended to manage users in a ZITADEL instance.\n" + "This API is intended to manage users in a ZITADEL instance.\n", }, - items: require("./docs/apis/resources/user_service_v2/sidebar.ts"), + items: sidebar_api_user_service_v2, }, { type: "category", - label: "Session Lifecycle", + label: "Session", link: { type: "generated-index", title: "Session Service API", slug: "/apis/resources/session_service_v2", description: - "This API is intended to manage sessions in a ZITADEL instance.\n" + "This API is intended to manage sessions in a ZITADEL instance.\n", }, - items: require("./docs/apis/resources/session_service_v2/sidebar.ts"), + items: sidebar_api_session_service_v2, }, { type: "category", - label: "OIDC Lifecycle", + label: "OIDC", link: { type: "generated-index", title: "OIDC Service API", slug: "/apis/resources/oidc_service_v2", description: - "Get OIDC Auth Request details and create callback URLs.\n" + "Get OIDC Auth Request details and create callback URLs.\n", }, - items: require("./docs/apis/resources/oidc_service_v2/sidebar.ts"), + items: sidebar_api_oidc_service_v2, }, { type: "category", - label: "Settings Lifecycle", + label: "Settings", link: { type: "generated-index", title: "Settings Service API", slug: "/apis/resources/settings_service_v2", description: - "This API is intended to manage settings in a ZITADEL instance.\n" + "This API is intended to manage settings in a ZITADEL instance.\n", }, - items: require("./docs/apis/resources/settings_service_v2/sidebar.ts"), + items: sidebar_api_settings_service_v2, }, { type: "category", - label: "Feature Lifecycle", + label: "Feature", link: { type: "generated-index", title: "Feature Service API", slug: "/apis/resources/feature_service_v2", description: - 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n' + 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n', }, - items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"), + items: sidebar_api_feature_service_v2, }, { type: "category", - label: "Organization Lifecycle", + label: "Organization", link: { type: "generated-index", title: "Organization Service API", slug: "/apis/resources/org_service/v2", description: - 'This API is intended to manage organizations for ZITADEL. \n' + "This API is intended to manage organizations for ZITADEL. \n", }, - items: require("./docs/apis/resources/org_service_v2/sidebar.ts"), + items: sidebar_api_org_service_v2, }, { type: "category", - label: "Identity Provider Lifecycle", + label: "Identity Provider", link: { type: "generated-index", title: "Identity Provider Service API", slug: "/apis/resources/idp_service_v2", description: - 'This API is intended to manage identity providers (IdPs) for ZITADEL.\n' + "This API is intended to manage identity providers (IdPs) for ZITADEL.\n", }, - items: require("./docs/apis/resources/idp_service_v2/sidebar.ts"), - }, - ], - }, - { - type: "category", - label: "V3 (Preview)", - collapsed: false, - link: { - type: "doc", - id: "apis/v3", - }, - items: [ - { - type: "category", - label: "User Schema Lifecycle (Preview)", - link: { - type: "generated-index", - title: "User Schema Service API (Preview)", - slug: "/apis/resources/user_schema_service", - description: - "This API is intended to manage data schemas for users in a ZITADEL instance.\n" + - "\n" + - "This project is in Preview state. It can AND will continue breaking until the service provides the same functionality as the v1 and v2 user services.", - }, - items: require("./docs/apis/resources/user_schema_service_v3/sidebar.ts"), + items: sidebar_api_idp_service_v2, }, { type: "category", - label: "User Lifecycle (Preview)", + label: "Web key (Beta)", link: { type: "generated-index", - title: "User Service API (Preview)", - slug: "/apis/resources/user_service_v3", + title: "Web Key Service API (Beta)", + slug: "/apis/resources/webkey_service_v2", description: - "This API is intended to manage users with your own data schema in a ZITADEL instance.\n" + - "\n" + - "This project is in Preview state. It can AND will continue breaking until the service provides the same functionality as the v1 and v2 user services.", + "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" + + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ + "\n"+ + "The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n"+ + "\n"+ + "Please make sure to enable the `web_key` feature flag on your instance to use this service and that you're running ZITADEL V3.", }, - items: require("./docs/apis/resources/user_service_v3/sidebar.ts"), + items: sidebar_api_webkey_service_v2 }, { type: "category", - label: "Action Lifecycle (Preview)", + label: "Action (Beta)", link: { type: "generated-index", - title: "Action Service API (Preview)", - slug: "/apis/resources/action_service_v3", + title: "Action Service API (Beta)", + slug: "/apis/resources/action_service_v2", description: - "This API is intended to manage custom executions and targets (previously known as actions) in a ZITADEL instance.\n" + - "The version 3 of actions provide much more options to customize ZITADELs behaviour than previous action versions.\n" + - "Also, v3 actions are available instance-wide, whereas previous actions had to be managed for each organization individually\n" + - "ZITADEL doesn't restrict the implementation languages, tooling and runtime for v3 action executions anymore.\n" + - "Instead, it calls external endpoints which are implemented and maintained by action v3 users.\n" + - "\n" + - "This project is in Preview state. It can AND will continue breaking until the services provide the same functionality as the current actions.", + "This API is intended to manage custom executions and targets (previously known as actions) in a ZITADEL instance.\n" + + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ + "\n" + + "The version 2 of actions provide much more options to customize ZITADELs behaviour than previous action versions.\n" + + "Also, v2 actions are available instance-wide, whereas previous actions had to be managed for each organization individually\n" + + "ZITADEL doesn't restrict the implementation languages, tooling and runtime for v2 action executions anymore.\n" + + "Instead, it calls external endpoints which are implemented and maintained by action v2 users.\n"+ + "\n" + + "Please make sure to enable the `actions` feature flag on your instance to use this service and that you're running Zitadel V3.", }, - items: [{ - type: "doc", - id: "apis/actions/v3/usage", - }, { - type: "doc", - id: "apis/actions/v3/testing-locally", - }].concat(require("./docs/apis/resources/action_service_v3/sidebar.ts")), - }, - { - type: "category", - label: "Web key Lifecycle (Preview)", - link: { - type: "generated-index", - title: "Web Key Service API (Preview)", - slug: "/apis/resources/webkey_service_v3", - description: - "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" + - "\n" + - "This project is in preview state. It can AND will continue breaking until a stable version is released.", - }, - items: require("./docs/apis/resources/webkey_service_v3/sidebar.ts"), + items: sidebar_api_actions_v2, }, ], }, @@ -823,7 +861,11 @@ module.exports = { collapsed: true, items: [ "apis/openidoauth/endpoints", - "apis/openidoauth/authrequest", + { + label: "OIDC Playground", + type: "link", + href: "https://zitadel.com/playgrounds/oidc", + }, "apis/openidoauth/scopes", "apis/openidoauth/claims", "apis/openidoauth/authn-methods", @@ -842,9 +884,7 @@ module.exports = { type: "category", label: "Provision Users", collapsed: true, - items: [ - 'apis/scim2' - ], + items: ["apis/scim2"], }, { type: "category", @@ -874,13 +914,13 @@ module.exports = { }, { type: "link", - label: "Rate Limits (Cloud)", // The link label - href: "/legal/policies/rate-limit-policy", // The internal path + label: "Rate Limits (Cloud)", + href: "/legal/policies/rate-limit-policy", }, { type: "category", label: "Benchmarks", - collapsed: false, + collapsed: false, link: { type: "doc", id: "apis/benchmarks/index", @@ -892,12 +932,9 @@ module.exports = { link: { title: "v2.65.0", slug: "/apis/benchmarks/v2.65.0", - description: - "Benchmark results of Zitadel v2.65.0\n" + description: "Benchmark results of Zitadel v2.65.0\n", }, - items: [ - "apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index", - ], + items: ["apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index"], }, { type: "category", @@ -905,11 +942,21 @@ module.exports = { link: { title: "v2.66.0", slug: "/apis/benchmarks/v2.66.0", - description: - "Benchmark results of Zitadel v2.66.0\n" + description: "Benchmark results of Zitadel v2.66.0\n", + }, + items: ["apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index"], + }, + { + type: "category", + label: "v2.70.0", + link: { + title: "v2.70.0", + slug: "/apis/benchmarks/v2.70.0", + description: "Benchmark results of Zitadel v2.70.0\n", }, items: [ - "apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index", + "apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index", + "apis/benchmarks/v2.70.0/oidc_session/index", ], }, ], @@ -952,7 +999,6 @@ module.exports = { "self-hosting/manage/reverseproxy/traefik/traefik", "self-hosting/manage/reverseproxy/nginx/nginx", "self-hosting/manage/reverseproxy/caddy/caddy", - // "self-hosting/manage/reverseproxy/httpd/httpd", grpc NOT WORKING "self-hosting/manage/reverseproxy/cloudflare/cloudflare", "self-hosting/manage/reverseproxy/cloudflare_tunnel/cloudflare_tunnel", "self-hosting/manage/reverseproxy/zitadel_cloud/zitadel_cloud", diff --git a/docs/src/components/authrequest.jsx b/docs/src/components/authrequest.jsx index 71fd1349ea..82ecc91337 100644 --- a/docs/src/components/authrequest.jsx +++ b/docs/src/components/authrequest.jsx @@ -25,7 +25,7 @@ const LinkButton = ({ return ( { + if (!dataPerMetric.has(result.metric_name)) { + dataPerMetric.set(result.metric_name, [ + [ + {type:"datetime", label: "timestamp"}, + {type:"number", label: "p50"}, + {type:"number", label: "p95"}, + {type:"number", label: "p99"}, + ], + ]); + } + if (result.p99 > maxVValue) { + maxVValue = result.p99; + } + dataPerMetric.get(result.metric_name).push([ + new Date(result.timestamp), + result.p50, + result.p95, + result.p99, + ]); + }); const options = { legend: { position: 'bottom' }, @@ -11,35 +35,27 @@ export function BenchmarkChart(testResults=[], height='500px') { }, vAxis: { title: 'latency (ms)', + maxValue: maxVValue, }, + title: '' }; - - const data = [ - [ - {type:"datetime", label: "timestamp"}, - {type:"number", label: "p50"}, - {type:"number", label: "p95"}, - {type:"number", label: "p99"}, - ], - ] - - JSON.parse(testResults.testResults).forEach((result) => { - data.push([ - new Date(result.timestamp), - result.p50, - result.p95, - result.p99, - ]) + const charts = []; + + dataPerMetric.forEach((data, metric) => { + const opt = Object.create(options); + opt.title = metric; + charts.push( + + ); }); - return ( - - ); + + return (charts); } \ No newline at end of file diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 5083c842e9..50035f2541 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -612,4 +612,32 @@ p strong { position: absolute; top: 0; left: 0; -} \ No newline at end of file +} + +/* + The overview list components are enriched by swizzled DocCardList and DocSidebarItems wrappers. + Deprecated list item titles have the class zitadel-lifecycle-deprecated. + We strike them through, because this is well-known practice and reduces mental overhead for finding the right method to solve a task. + */ +.card:has(.zitadel-lifecycle-deprecated) { + position: relative; +} + +.card:has(.zitadel-lifecycle-deprecated)::after { + content: "DEPRECATED"; + position: absolute; + top: 10px; + right: 10px; + background-color: var(--ifm-color-danger-contrast-background); + color: var(--ifm-color-danger-contrast-foreground); + font-size: 12px; + font-weight: 600; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.zitadel-lifecycle-deprecated { + text-decoration: line-through; +} diff --git a/docs/src/theme/DocCardList/index.js b/docs/src/theme/DocCardList/index.js new file mode 100644 index 0000000000..c9c24e2b8f --- /dev/null +++ b/docs/src/theme/DocCardList/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import DocCardList from '@theme-original/DocCardList'; +import toCustomDeprecatedItemsProps from "../../utils/deprecated-items"; + +// The DocCardList component is used in generated index pages for API services. +// We customize it for deprecated items. +export default function DocCardListWrapper(props) { + return ( + <> + + + ); +} diff --git a/docs/src/theme/DocSidebarItems/index.js b/docs/src/theme/DocSidebarItems/index.js new file mode 100644 index 0000000000..35ad429fa2 --- /dev/null +++ b/docs/src/theme/DocSidebarItems/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import DocSidebarItems from '@theme-original/DocSidebarItems'; +import toCustomDeprecatedItemsProps from '../../utils/deprecated-items.js'; + +// The DocSidebarItems component is used in generated side navs for API services. +// We wrap the original to push deprecated items to the bottom and give them a CSS class. +// This lets us easily style them differently in docs/src/css/custom.css. +export default function DocSidebarItemsWrapper(props) { + return ( + <> + + + ); +} diff --git a/docs/src/utils/deprecated-items.js b/docs/src/utils/deprecated-items.js new file mode 100644 index 0000000000..353a619d30 --- /dev/null +++ b/docs/src/utils/deprecated-items.js @@ -0,0 +1,29 @@ +import React from "react"; + +// This function changes a ListComponents input properties. +// Deprecated items are pushed to the bottom of the list and its labels are given the CSS class zitadel-lifecycle-deprecated. +// They are styled in docs/src/css/custom.css. +export default function (props) { + const { items = [], ...rest } = props; + if (!Array.isArray(items)) { + // Do nothing if items is not an array + return props; + } + const withDeprecated = [...items].map(({className, label, ...itemRest}) => { + const zitadelLifecycleDeprecated = className?.indexOf('menu__list-item--deprecated') > -1 + const wrappedLabel = {label} + return { + zitadelLifecycleDeprecated: zitadelLifecycleDeprecated, + ...itemRest, + className, + label: wrappedLabel, + }; + }); + const sortedItems = [...withDeprecated].sort((a, b) => { + return a.zitadelLifecycleDeprecated - b.zitadelLifecycleDeprecated; + }); + return { + ...rest, + items: sortedItems, + }; +} diff --git a/docs/static/img/guides/console/invitehuman.png b/docs/static/img/guides/console/invitehuman.png new file mode 100644 index 0000000000..27857ced7e Binary files /dev/null and b/docs/static/img/guides/console/invitehuman.png differ diff --git a/docs/static/img/guides/console/setupauthmethod.png b/docs/static/img/guides/console/setupauthmethod.png new file mode 100644 index 0000000000..1c900f4662 Binary files /dev/null and b/docs/static/img/guides/console/setupauthmethod.png differ diff --git a/docs/static/img/guides/login-ui/device-auth-flow.png b/docs/static/img/guides/login-ui/device-auth-flow.png new file mode 100644 index 0000000000..f240df32f4 Binary files /dev/null and b/docs/static/img/guides/login-ui/device-auth-flow.png differ diff --git a/docs/static/img/guides/login-ui/oidc-flow.png b/docs/static/img/guides/login-ui/oidc-flow.png index b606044770..a427bad4ef 100644 Binary files a/docs/static/img/guides/login-ui/oidc-flow.png and b/docs/static/img/guides/login-ui/oidc-flow.png differ diff --git a/docs/static/img/guides/login-ui/saml-flow.png b/docs/static/img/guides/login-ui/saml-flow.png index 5c91fb4430..2a42642e2d 100644 Binary files a/docs/static/img/guides/login-ui/saml-flow.png and b/docs/static/img/guides/login-ui/saml-flow.png differ diff --git a/docs/static/img/guides/quickstart/20.png b/docs/static/img/guides/quickstart/20.png deleted file mode 100644 index 8ad3af1c50..0000000000 Binary files a/docs/static/img/guides/quickstart/20.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/21.png b/docs/static/img/guides/quickstart/21.png deleted file mode 100644 index b27a5d878d..0000000000 Binary files a/docs/static/img/guides/quickstart/21.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/38.png b/docs/static/img/guides/quickstart/38.png deleted file mode 100644 index 5c588a4235..0000000000 Binary files a/docs/static/img/guides/quickstart/38.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/39.png b/docs/static/img/guides/quickstart/39.png deleted file mode 100644 index 8de05371e6..0000000000 Binary files a/docs/static/img/guides/quickstart/39.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/41.png b/docs/static/img/guides/quickstart/41.png deleted file mode 100644 index 297cf57e8a..0000000000 Binary files a/docs/static/img/guides/quickstart/41.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/42.png b/docs/static/img/guides/quickstart/42.png deleted file mode 100644 index ff62d1001e..0000000000 Binary files a/docs/static/img/guides/quickstart/42.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/43.png b/docs/static/img/guides/quickstart/43.png deleted file mode 100644 index 1df261b869..0000000000 Binary files a/docs/static/img/guides/quickstart/43.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/44.png b/docs/static/img/guides/quickstart/44.png deleted file mode 100644 index 50bcb1a05e..0000000000 Binary files a/docs/static/img/guides/quickstart/44.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/45.png b/docs/static/img/guides/quickstart/45.png deleted file mode 100644 index 28ce863f0d..0000000000 Binary files a/docs/static/img/guides/quickstart/45.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/46.png b/docs/static/img/guides/quickstart/46.png deleted file mode 100644 index ca78190a75..0000000000 Binary files a/docs/static/img/guides/quickstart/46.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/47.png b/docs/static/img/guides/quickstart/47.png deleted file mode 100644 index a4ce71bde6..0000000000 Binary files a/docs/static/img/guides/quickstart/47.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/48.png b/docs/static/img/guides/quickstart/48.png deleted file mode 100644 index a9a48a496b..0000000000 Binary files a/docs/static/img/guides/quickstart/48.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/49.png b/docs/static/img/guides/quickstart/49.png deleted file mode 100644 index 05cf9af65d..0000000000 Binary files a/docs/static/img/guides/quickstart/49.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/50.png b/docs/static/img/guides/quickstart/50.png deleted file mode 100644 index 73ddb1e9ca..0000000000 Binary files a/docs/static/img/guides/quickstart/50.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/app_detail_overview_react.png b/docs/static/img/guides/quickstart/app_detail_overview_react.png new file mode 100644 index 0000000000..b529f5338c Binary files /dev/null and b/docs/static/img/guides/quickstart/app_detail_overview_react.png differ diff --git a/docs/static/img/guides/quickstart/create_instance.png b/docs/static/img/guides/quickstart/create_instance.png new file mode 100644 index 0000000000..7f7d1847e6 Binary files /dev/null and b/docs/static/img/guides/quickstart/create_instance.png differ diff --git a/docs/static/img/guides/quickstart/create_instance_admin.png b/docs/static/img/guides/quickstart/create_instance_admin.png new file mode 100644 index 0000000000..320228299b Binary files /dev/null and b/docs/static/img/guides/quickstart/create_instance_admin.png differ diff --git a/docs/static/img/guides/quickstart/create_instance_name.png b/docs/static/img/guides/quickstart/create_instance_name.png new file mode 100644 index 0000000000..b9a6c94181 Binary files /dev/null and b/docs/static/img/guides/quickstart/create_instance_name.png differ diff --git a/docs/static/img/guides/quickstart/create_project_react.png b/docs/static/img/guides/quickstart/create_project_react.png new file mode 100644 index 0000000000..afc52a9f35 Binary files /dev/null and b/docs/static/img/guides/quickstart/create_project_react.png differ diff --git a/docs/static/img/guides/quickstart/instance_dashboard.png b/docs/static/img/guides/quickstart/instance_dashboard.png new file mode 100644 index 0000000000..f57acb386d Binary files /dev/null and b/docs/static/img/guides/quickstart/instance_dashboard.png differ diff --git a/docs/static/img/guides/quickstart/onboarding_questions.png b/docs/static/img/guides/quickstart/onboarding_questions.png new file mode 100644 index 0000000000..47c7d69d64 Binary files /dev/null and b/docs/static/img/guides/quickstart/onboarding_questions.png differ diff --git a/docs/static/img/guides/quickstart/project_config_overview_react.png b/docs/static/img/guides/quickstart/project_config_overview_react.png new file mode 100644 index 0000000000..018b6e5b35 Binary files /dev/null and b/docs/static/img/guides/quickstart/project_config_overview_react.png differ diff --git a/docs/static/img/guides/quickstart/sign_in_instance.png b/docs/static/img/guides/quickstart/sign_in_instance.png new file mode 100644 index 0000000000..1ee0860957 Binary files /dev/null and b/docs/static/img/guides/quickstart/sign_in_instance.png differ diff --git a/docs/static/img/guides/quickstart/v3_10.png b/docs/static/img/guides/quickstart/v3_10.png deleted file mode 100644 index 4c05106001..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_10.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/v3_11.png b/docs/static/img/guides/quickstart/v3_11.png deleted file mode 100644 index e49c81cc07..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_11.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/v3_12.png b/docs/static/img/guides/quickstart/v3_12.png deleted file mode 100644 index eded356f0c..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_12.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/v3_16.png b/docs/static/img/guides/quickstart/v3_16.png deleted file mode 100644 index 30be6d0635..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_16.png and /dev/null differ diff --git a/docs/static/img/guides/quickstart/v3_9.png b/docs/static/img/guides/quickstart/v3_9.png deleted file mode 100644 index 7d754b2aec..0000000000 Binary files a/docs/static/img/guides/quickstart/v3_9.png and /dev/null differ diff --git a/docs/static/img/manuals/portal/customer_portal_add_admin.png b/docs/static/img/manuals/portal/customer_portal_add_admin.png index 8228810a56..cf3c497b13 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_add_admin.png and b/docs/static/img/manuals/portal/customer_portal_add_admin.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_add_domain.png b/docs/static/img/manuals/portal/customer_portal_add_domain.png index 2e83a599ca..2296cc629c 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_add_domain.png and b/docs/static/img/manuals/portal/customer_portal_add_domain.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_administrator_list.png b/docs/static/img/manuals/portal/customer_portal_administrator_list.png index e8fcc6bac8..c1aaf13a77 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_administrator_list.png and b/docs/static/img/manuals/portal/customer_portal_administrator_list.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_billing.png b/docs/static/img/manuals/portal/customer_portal_billing.png index 09f1cf9f9f..2f0e19068f 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_billing.png and b/docs/static/img/manuals/portal/customer_portal_billing.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_delete_admin.png b/docs/static/img/manuals/portal/customer_portal_delete_admin.png index cdaf75cf74..e3c6ac88a7 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_delete_admin.png and b/docs/static/img/manuals/portal/customer_portal_delete_admin.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_general_support.png b/docs/static/img/manuals/portal/customer_portal_general_support.png index c3e8241258..8aeb0a1711 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_general_support.png and b/docs/static/img/manuals/portal/customer_portal_general_support.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_instance_detail.png b/docs/static/img/manuals/portal/customer_portal_instance_detail.png index 48a9a2fd39..f33d7f26dd 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_instance_detail.png and b/docs/static/img/manuals/portal/customer_portal_instance_detail.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_instance_overview.png b/docs/static/img/manuals/portal/customer_portal_instance_overview.png index 973c6b9205..7360fadbb0 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_instance_overview.png and b/docs/static/img/manuals/portal/customer_portal_instance_overview.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_invoices.png b/docs/static/img/manuals/portal/customer_portal_invoices.png index 80942fdb24..4cb72e118d 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_invoices.png and b/docs/static/img/manuals/portal/customer_portal_invoices.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_settings_general.png b/docs/static/img/manuals/portal/customer_portal_settings_general.png index f1daa340db..a9aaf068cd 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_settings_general.png and b/docs/static/img/manuals/portal/customer_portal_settings_general.png differ diff --git a/docs/static/img/manuals/portal/customer_portal_upgrade_tier.png b/docs/static/img/manuals/portal/customer_portal_upgrade_tier.png index 8f84c79545..382a5bcbb6 100644 Binary files a/docs/static/img/manuals/portal/customer_portal_upgrade_tier.png and b/docs/static/img/manuals/portal/customer_portal_upgrade_tier.png differ diff --git a/docs/static/img/tech/fastapi.svg b/docs/static/img/tech/fastapi.svg new file mode 100644 index 0000000000..85f2d13372 --- /dev/null +++ b/docs/static/img/tech/fastapi.svg @@ -0,0 +1 @@ + diff --git a/docs/static/img/zitadel_cluster_architecture.png b/docs/static/img/zitadel_cluster_architecture.png new file mode 100644 index 0000000000..a6d63af501 Binary files /dev/null and b/docs/static/img/zitadel_cluster_architecture.png differ diff --git a/docs/static/img/zitadel_multicluster_architecture.png b/docs/static/img/zitadel_multicluster_architecture.png new file mode 100644 index 0000000000..22b2ead4f5 Binary files /dev/null and b/docs/static/img/zitadel_multicluster_architecture.png differ diff --git a/docs/vercel.json b/docs/vercel.json index 58a13e4b8c..039dc02476 100644 --- a/docs/vercel.json +++ b/docs/vercel.json @@ -24,8 +24,8 @@ { "source": "/docs/apis/auth/:slug*", "destination": "/docs/apis/resources/auth/:slug*", "permanent": true }, { "source": "/docs/apis/system/:slug*", "destination": "/docs/apis/resources/system/:slug*", "permanent": true }, { "source": "/docs/apis/admin/:slug*", "destination": "/docs/apis/resources/admin/:slug*", "permanent": true }, - { "source": "/docs/apis/actionsv2/introduction", "destination": "/docs/apis/actions/v3/usage", "permanent": true }, - { "source": "/docs/apis/actionsv2/execution-local", "destination": "/docs/apis/actions/v3/testing-locally", "permanent": true }, + { "source": "/docs/apis/actionsv2/introduction", "destination": "/docs/apis/actions/v2/usage", "permanent": true }, + { "source": "/docs/apis/actionsv2/execution-local", "destination": "/docs/apis/actions/v2/testing-locally", "permanent": true }, { "source": "/docs/guides/integrate/human-users", "destination": "/docs/guides/integrate/login", "permanent": true }, { "source": "/docs/guides/solution-scenarios/device-authorization", "destination": "/docs/guides/integrate/login/oidc/device-authorization", "permanent": true }, { "source": "/docs/guides/integrate/oauth-recommended-flows", "destination": "/docs/guides/integrate/login/oidc/oauth-recommended-flows", "permanent": true }, diff --git a/docs/yarn.lock b/docs/yarn.lock index 94698e4821..ad31e03b5e 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -185,6 +185,15 @@ "@babel/highlight" "^7.24.7" picocolors "^1.0.0" +"@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" @@ -389,11 +398,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" @@ -410,12 +429,12 @@ "@babel/types" "^7.24.7" "@babel/helpers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416" - integrity sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg== + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" + integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg== dependencies: - "@babel/template" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/template" "^7.27.0" + "@babel/types" "^7.27.0" "@babel/highlight@^7.24.7": version "7.24.7" @@ -432,6 +451,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== +"@babel/parser@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" + integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== + dependencies: + "@babel/types" "^7.27.0" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" @@ -1192,9 +1218,9 @@ regenerator-runtime "^0.14.0" "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.22.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" - integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" + integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== dependencies: regenerator-runtime "^0.14.0" @@ -1207,6 +1233,15 @@ "@babel/parser" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/template@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" + integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.27.0" + "@babel/types" "^7.27.0" + "@babel/traverse@^7.22.8", "@babel/traverse@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" @@ -1232,6 +1267,14 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" +"@babel/types@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" + integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@braintree/sanitize-url@^6.0.1": version "6.0.4" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" @@ -3409,9 +3452,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629: - version "1.0.30001636" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" - integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== + version "1.0.30001702" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz" + integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== ccount@^2.0.0: version "2.0.1" @@ -6153,9 +6196,9 @@ ignore@^5.2.0, ignore@^5.2.4: integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== image-size@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.1.1.tgz#ddd67d4dc340e52ac29ce5f546a09f4e29e840ac" - integrity sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.1.tgz#ee118aedfe666db1a6ee12bed5821cde3740276d" + integrity sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw== dependencies: queue "6.0.2" @@ -8156,9 +8199,9 @@ mz@^2.7.0: thenify-all "^1.0.0" nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== negotiator@0.6.3: version "0.6.3" diff --git a/e2e/config/host.docker.internal/zitadel.yaml b/e2e/config/host.docker.internal/zitadel.yaml index cb7e985be1..203dd16437 100644 --- a/e2e/config/host.docker.internal/zitadel.yaml +++ b/e2e/config/host.docker.internal/zitadel.yaml @@ -5,12 +5,23 @@ ExternalDomain: host.docker.internal ExternalSecure: false Database: - cockroach: + postgres: # This makes the e2e config reusable with an out-of-docker zitadel process and an /etc/hosts entry - Host: host.docker.internal - EventPushConnRatio: 0.2 + Host: host.docker.internal + Port: 5432 MaxOpenConns: 15 MaxIdleConns: 10 + Database: zitadel + User: + Username: zitadel + Password: zitadel + SSL: + Mode: disable + Admin: + Username: postgres + Password: postgres + SSL: + Mode: disable TLS: Enabled: false diff --git a/e2e/config/localhost/docker-compose.yaml b/e2e/config/localhost/docker-compose.yaml index f90ee158f0..41334d92f9 100644 --- a/e2e/config/localhost/docker-compose.yaml +++ b/e2e/config/localhost/docker-compose.yaml @@ -30,14 +30,15 @@ services: db: restart: 'always' - image: 'cockroachdb/cockroach:latest' - command: 'start-single-node --insecure --http-addr :9090' + image: 'postgres:17-alpine' + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:9090/health?ready=1'] + test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"] interval: '10s' timeout: '30s' retries: 5 start_period: '20s' ports: - - "26257:26257" - - "9090:9090" + - "5432:5432" diff --git a/e2e/config/localhost/zitadel.yaml b/e2e/config/localhost/zitadel.yaml index 649f35fa9d..966bb4f6b7 100644 --- a/e2e/config/localhost/zitadel.yaml +++ b/e2e/config/localhost/zitadel.yaml @@ -5,12 +5,24 @@ ExternalDomain: localhost ExternalSecure: false Database: - cockroach: + postgres: # This makes the e2e config reusable with an out-of-docker zitadel process and an /etc/hosts entry Host: host.docker.internal - EventPushConnRatio: 0.2 + Port: 5432 + database: zitadel MaxOpenConns: 15 MaxIdleConns: 10 + Database: zitadel + User: + Username: zitadel + Password: zitadel + SSL: + Mode: disable + Admin: + Username: postgres + Password: postgres + SSL: + Mode: disable TLS: Enabled: false diff --git a/e2e/cypress/e2e/machines/machines.cy.ts b/e2e/cypress/e2e/machines/machines.cy.ts index 504dc88df2..4ffe06feea 100644 --- a/e2e/cypress/e2e/machines/machines.cy.ts +++ b/e2e/cypress/e2e/machines/machines.cy.ts @@ -56,8 +56,8 @@ describe('machines', () => { loginName = loginname(machine.removeName, Cypress.env('ORGANIZATION')); } it('should delete a machine', () => { - const rowSelector = `tr:contains(${machine.removeName})`; - cy.get(rowSelector).find('[data-e2e="enabled-delete-button"]').click({ force: true }); + const rowSelector = `tr:contains('${machine.removeName}')`; + cy.get(rowSelector).should('be.visible').find('[data-e2e="enabled-delete-button"]').click({ force: true }); cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(loginName); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.shouldConfirmSuccess(); diff --git a/e2e/yarn.lock b/e2e/yarn.lock index 17bb06236d..53932921e9 100644 --- a/e2e/yarn.lock +++ b/e2e/yarn.lock @@ -181,9 +181,9 @@ aws4@^1.8.0: integrity sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA== axios@^1.6.1: - version "1.7.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" - integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== + version "1.8.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" + integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" diff --git a/go.mod b/go.mod index 0887e83c47..5943f8f85e 100644 --- a/go.mod +++ b/go.mod @@ -1,156 +1,172 @@ module github.com/zitadel/zitadel -go 1.23.4 +go 1.23.7 require ( - cloud.google.com/go/profiler v0.4.1 - cloud.google.com/go/storage v1.43.0 - github.com/BurntSushi/toml v1.4.0 + cloud.google.com/go/profiler v0.4.2 + cloud.google.com/go/storage v1.51.0 + github.com/BurntSushi/toml v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.24.0 - github.com/Masterminds/semver/v3 v3.3.1 + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0 + github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/squirrel v1.5.4 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b - github.com/alecthomas/participle/v2 v2.1.1 - github.com/alicebob/miniredis/v2 v2.33.0 + github.com/alecthomas/participle/v2 v2.1.4 + github.com/alicebob/miniredis/v2 v2.34.0 github.com/benbjohnson/clock v1.3.5 github.com/boombuler/barcode v1.0.2 github.com/brianvoe/gofakeit/v6 v6.28.0 - github.com/cockroachdb/cockroach-go/v2 v2.3.8 + github.com/cockroachdb/cockroach-go/v2 v2.4.0 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/crewjam/saml v0.4.14 - github.com/descope/virtualwebauthn v1.0.2 - github.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2 - github.com/dop251/goja_nodejs v0.0.0-20240418154818-2aae10d4cbcf + github.com/descope/virtualwebauthn v1.0.3 + github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c + github.com/dop251/goja_nodejs v0.0.0-20250314160716-c55ecee183c0 github.com/drone/envsubst v1.0.3 - github.com/envoyproxy/protoc-gen-validate v1.0.4 - github.com/fatih/color v1.17.0 - github.com/gabriel-vasile/mimetype v1.4.4 - github.com/go-chi/chi/v5 v5.1.0 - github.com/go-jose/go-jose/v4 v4.0.4 - github.com/go-ldap/ldap/v3 v3.4.8 + github.com/envoyproxy/protoc-gen-validate v1.2.1 + github.com/fatih/color v1.18.0 + github.com/fergusstrange/embedded-postgres v1.30.0 + github.com/gabriel-vasile/mimetype v1.4.8 + github.com/go-chi/chi/v5 v5.2.1 + github.com/go-jose/go-jose/v4 v4.0.5 + github.com/go-ldap/ldap/v3 v3.4.10 github.com/go-webauthn/webauthn v0.10.2 - github.com/goccy/go-json v0.10.3 + github.com/goccy/go-json v0.10.5 github.com/golang/protobuf v1.5.4 github.com/gorilla/csrf v1.7.2 github.com/gorilla/mux v1.8.1 github.com/gorilla/schema v1.4.1 github.com/gorilla/securecookie v1.1.2 - github.com/gorilla/websocket v1.4.1 + github.com/gorilla/websocket v1.5.3 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 github.com/h2non/gock v1.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/improbable-eng/grpc-web v0.15.0 - github.com/jackc/pgx/v5 v5.7.2 + github.com/jackc/pgx/v5 v5.7.3 github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 github.com/jinzhu/gorm v1.9.16 github.com/k3a/html2text v1.2.1 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/manifoldco/promptui v0.9.0 - github.com/minio/minio-go/v7 v7.0.73 + github.com/minio/minio-go/v7 v7.0.88 github.com/mitchellh/mapstructure v1.5.0 github.com/muesli/gamut v0.3.1 github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 github.com/nicksnyder/go-i18n/v2 v2.4.0 - github.com/pashagolub/pgxmock/v4 v4.3.0 + github.com/pashagolub/pgxmock/v4 v4.6.0 github.com/pquerna/otp v1.4.0 github.com/rakyll/statik v0.1.7 - github.com/redis/go-redis/v9 v9.7.0 - github.com/riverqueue/river v0.16.0 - github.com/riverqueue/river/riverdriver v0.16.0 + github.com/redis/go-redis/v9 v9.7.3 + github.com/riverqueue/river v0.19.0 + github.com/riverqueue/river/riverdriver v0.19.0 + github.com/riverqueue/river/rivertype v0.19.0 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 - github.com/sony/gobreaker/v2 v2.0.0 + github.com/sony/gobreaker/v2 v2.1.0 github.com/sony/sonyflake v1.2.0 - github.com/spf13/cobra v1.8.1 - github.com/spf13/viper v1.19.0 + github.com/spf13/cobra v1.9.1 + github.com/spf13/viper v1.20.0 github.com/stretchr/testify v1.10.0 - github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 - github.com/twilio/twilio-go v1.22.2 - github.com/zitadel/logging v0.6.1 - github.com/zitadel/oidc/v3 v3.32.0 - github.com/zitadel/passwap v0.6.0 - github.com/zitadel/saml v0.3.3 - github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 - go.opentelemetry.io/otel v1.29.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 - go.opentelemetry.io/otel/exporters/prometheus v0.50.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0 - go.opentelemetry.io/otel/metric v1.29.0 - go.opentelemetry.io/otel/sdk v1.29.0 - go.opentelemetry.io/otel/sdk/metric v1.29.0 - go.opentelemetry.io/otel/trace v1.29.0 + github.com/twilio/twilio-go v1.24.1 + github.com/zitadel/exifremove v0.1.0 + github.com/zitadel/logging v0.6.2 + github.com/zitadel/oidc/v3 v3.36.1 + github.com/zitadel/passwap v0.7.0 + github.com/zitadel/saml v0.3.5 + github.com/zitadel/schema v1.3.1 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 + go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 + go.opentelemetry.io/otel/exporters/prometheus v0.57.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 + go.opentelemetry.io/otel/metric v1.35.0 + go.opentelemetry.io/otel/sdk v1.35.0 + go.opentelemetry.io/otel/sdk/metric v1.35.0 + go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/mock v0.5.0 - golang.org/x/crypto v0.31.0 - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - golang.org/x/net v0.33.0 - golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.11.0 - golang.org/x/text v0.21.0 - google.golang.org/api v0.187.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd - google.golang.org/grpc v1.65.0 - google.golang.org/protobuf v1.34.2 + golang.org/x/crypto v0.36.0 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 + golang.org/x/net v0.37.0 + golang.org/x/oauth2 v0.28.0 + golang.org/x/sync v0.12.0 + golang.org/x/text v0.23.0 + google.golang.org/api v0.227.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 + google.golang.org/grpc v1.71.0 + google.golang.org/protobuf v1.36.5 sigs.k8s.io/yaml v1.4.0 ) require ( - cloud.google.com/go/auth v0.6.1 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.0 // indirect - github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect - github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect + cel.dev/expr v0.19.2 // indirect + cloud.google.com/go/auth v0.15.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/monitoring v1.24.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect + github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/chzyer/readline v1.5.1 // indirect + github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect github.com/crewjam/httperr v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-redsync/redsync/v4 v4.13.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-webauthn/x v0.1.9 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/go-tpm v0.9.0 // indirect - github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect - github.com/google/s2a-go v0.1.7 // indirect + github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/crc64nvme v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/riverqueue/river/rivershared v0.16.0 // indirect - github.com/riverqueue/river/rivertype v0.16.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/riverqueue/river/rivershared v0.19.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zenazn/goji v1.0.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect + go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/time v0.5.0 // indirect - google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect ) require ( - cloud.google.com/go v0.115.0 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect - cloud.google.com/go/iam v1.1.8 // indirect - cloud.google.com/go/trace v1.10.7 // indirect + cloud.google.com/go v0.118.3 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.4.1 // indirect + cloud.google.com/go/trace v1.11.3 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/amdonov/xmlsig v0.1.0 // indirect github.com/beevik/etree v1.3.0 @@ -159,7 +175,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect @@ -170,56 +186,50 @@ require ( github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.6.0 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect - github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect - github.com/gofrs/flock v0.8.1 // indirect - github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect + github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7 // indirect github.com/google/uuid v1.6.0 - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jonboulle/clockwork v0.4.0 - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect github.com/muesli/kmeans v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/riverqueue/river/riverdriver/riverpgxv5 v0.16.0 - github.com/rs/xid v1.5.0 // indirect + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.19.0 + github.com/rs/xid v1.6.0 // indirect github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 github.com/subosito/gotenv v1.6.0 // indirect github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/sys v0.28.0 - gopkg.in/ini.v1 v1.67.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + golang.org/x/sys v0.31.0 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect nhooyr.io/websocket v1.8.11 // indirect diff --git a/go.sum b/go.sum index eadfe58741..646098db59 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,51 @@ +cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= +cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= -cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= -cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38= -cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= -cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/logging v1.10.0 h1:f+ZXMqyrSJ5vZ5pE/zr0xC8y/M9BLNzQeLBwfeZ+wY4= -cloud.google.com/go/logging v1.10.0/go.mod h1:EHOwcxlltJrYGqMGfghSet736KR3hX1MAj614mrMk9I= -cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= -cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= -cloud.google.com/go/monitoring v1.19.0 h1:NCXf8hfQi+Kmr56QJezXRZ6GPb80ZI7El1XztyUuLQI= -cloud.google.com/go/monitoring v1.19.0/go.mod h1:25IeMR5cQ5BoZ8j1eogHE5VPJLlReQ7zFp5OiLgiGZw= -cloud.google.com/go/profiler v0.4.1 h1:Q7+lOvikTGMJ/IAWocpYYGit4SIIoILmVZfEEWTORSY= -cloud.google.com/go/profiler v0.4.1/go.mod h1:LBrtEX6nbvhv1w/e5CPZmX9ajGG9BGLtGbv56Tg4SHs= -cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= -cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= -cloud.google.com/go/trace v1.10.7 h1:gK8z2BIJQ3KIYGddw9RJLne5Fx0FEXkrEQzPaeEYVvk= -cloud.google.com/go/trace v1.10.7/go.mod h1:qk3eiKmZX0ar2dzIJN/3QhY2PIFh1eqcIdaN5uEjQPM= +cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= +cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM= +cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= +cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= +cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= +cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= +cloud.google.com/go/profiler v0.4.2 h1:KojCmZ+bEPIQrd7bo2UFvZ2xUPLHl55KzHl7iaR4V2I= +cloud.google.com/go/profiler v0.4.2/go.mod h1:7GcWzs9deJHHdJ5J9V1DzKQ9JoIoTGhezwlLbwkOoCs= +cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= +cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= +cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= +cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.24.0 h1:TBo1ql03qmVkZzEndpfkS4i9dOgCVvO0rQP7HEth110= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.24.0/go.mod h1:pix4dhb6R3oDGZgQhkEGGC+5ZTz6kcxOhS4lhsSJhrE= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.0 h1:3vze4eFE3z2tDy2iSeI7yCQ17L8iLxN4OkXgvTr979s= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.0/go.mod h1:PdB0wkmILI+phhoBhWdrrB4LfORT9tHc03OOn+q3dWU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.0 h1:ng6QH9Z4bAXCf0Z1cjR5hKESyc1BUiOrfIOhN+nHfRU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.0/go.mod h1:ZC7rjqRzdhRKDK223jQ7Tsz89ZtrSSLH/VFzf7k5Sb0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0 h1:Jtr816GUk6+I2ox9L/v+VcOwN6IyGOEDTSNHfD6m9sY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0/go.mod h1:E05RN++yLx9W4fXPtX978OLo9P0+fBacauUdET1BckA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= @@ -51,12 +57,12 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= -github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= -github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= +github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -64,10 +70,10 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= -github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= +github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= github.com/amdonov/xmlsig v0.1.0 h1:i0iQ3neKLmUhcfIRgiiR3eRPKgXZj+n5lAfqnfKoeXI= github.com/amdonov/xmlsig v0.1.0/go.mod h1:jTR/jO0E8fSl/cLvMesP+RjxyV4Ux4WL1Ip64ZnQpA0= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= @@ -92,8 +98,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= -github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -124,8 +130,10 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38 github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/cockroach-go/v2 v2.3.8 h1:53yoUo4+EtrC1NrAEgnnad4AS3ntNvGup1PAXZ7UmpE= -github.com/cockroachdb/cockroach-go/v2 v2.3.8/go.mod h1:9uH5jK4yQ3ZQUT9IXe4I2fHzMIF5+JC/oOdzTRgJYJk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/cockroach-go/v2 v2.4.0 h1:7K5vpE3m7LylIbmpbr4eEhApDTPMgFgR+eDPy1sdJjM= +github.com/cockroachdb/cockroach-go/v2 v2.4.0/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= @@ -134,7 +142,7 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= @@ -147,22 +155,21 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/descope/virtualwebauthn v1.0.2 h1:cAvfS9wHh6On9HAE4Gjn3fJkf8MPQW2LzN8BPKEPs0M= -github.com/descope/virtualwebauthn v1.0.2/go.mod h1:iJvinjD1iZYqQ09J5lF0+795OdDbzTWcYQjPD/BF54M= +github.com/descope/virtualwebauthn v1.0.3 h1:rXm60q6D/GHiNyPzVifV9XSRQ8UhIR3wkel6HMlNvXE= +github.com/descope/virtualwebauthn v1.0.3/go.mod h1:xdLpAreAuRj5YEj/toVygZ2YX1S7d0l6AyKt3TJordg= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2 h1:4Ew88p5s9dwIk5/woUyqI9BD89NgZoUNH4/rM/h2UDg= -github.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw= -github.com/dop251/goja_nodejs v0.0.0-20240418154818-2aae10d4cbcf h1:2JoVYP9iko8uuIW33BQafzaylDixXbdXCRw/vCoxL+s= -github.com/dop251/goja_nodejs v0.0.0-20240418154818-2aae10d4cbcf/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg= +github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/dop251/goja_nodejs v0.0.0-20250314160716-c55ecee183c0 h1:jTwdYTGERaZ/3+glBUVQZV2NwGodd9HlkXJbTBUPLLo= +github.com/dop251/goja_nodejs v0.0.0-20250314160716-c55ecee183c0/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= -github.com/dsoprea/go-exif v0.0.0-20210131231135-d154f10435cc/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk= github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= @@ -174,7 +181,6 @@ github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SN github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= -github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210128210355-86b1014917f2/go.mod h1:ZoOP3yUG0HD1T4IUjIFsz/2OAB2yB4YX6NSm4K+uJRg= github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 h1:P1dsxzctGkmG6Zf7gH2xrZhNXWP5/FuLDI7xbCGsWTo= github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0= github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= @@ -184,10 +190,8 @@ github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3P github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= -github.com/dsoprea/go-png-image-structure v0.0.0-20200807080309-a98d4e94ac82/go.mod h1:aDYQkL/5gfRNZkoxiLTSWU4Y8/gV/4MVsy/MU9uwTak= github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA= github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw= -github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4= github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= @@ -203,34 +207,42 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= -github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fergusstrange/embedded-postgres v1.30.0 h1:ewv1e6bBlqOIYtgGgRcEnNDpfGlmfPxB8T3PO9tV68Q= +github.com/fergusstrange/embedded-postgres v1.30.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= -github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= -github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= -github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= @@ -239,14 +251,14 @@ github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= -github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= -github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= +github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= +github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -259,6 +271,14 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= +github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA= +github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ= github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -266,43 +286,42 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4= github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs= github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE= github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= -github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= +github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= -github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= -github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= +github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7 h1:kG/6mhO8OwbQrA/0XEPwKJs3D3jG0m1rNH/ZRKDA/pQ= +github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7/go.mod h1:J+F9/3Ofc8ysEOY2/cNjxTMl2eB1gvPIywEHUplPgDA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= @@ -322,6 +341,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -329,10 +350,10 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -340,19 +361,19 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g= -github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= -github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -371,8 +392,9 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= @@ -381,9 +403,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= -github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= @@ -393,10 +414,14 @@ github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplb github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -411,8 +436,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= @@ -433,8 +456,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo= +github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 h1:jny9eqYPwkG8IVy7foUoRjQmFLcArCSz+uPsL6KS0HQ= @@ -451,16 +474,16 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= -github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= +github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= @@ -484,11 +507,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -501,6 +524,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -516,8 +541,6 @@ github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= @@ -537,10 +560,12 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.73 h1:qr2vi96Qm7kZ4v7LLebjte+MQh621fFWnv93p12htEo= -github.com/minio/minio-go/v7 v7.0.73/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= +github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs= +github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= @@ -600,11 +625,11 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s= -github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= +github.com/pashagolub/pgxmock/v4 v4.6.0 h1:ds0hIs+bJtkfo01vqjp0BOFirjt4Ea8XV082uorzM3w= +github.com/pashagolub/pgxmock/v4 v4.6.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -614,6 +639,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -625,8 +652,8 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -640,8 +667,8 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -653,41 +680,43 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= -github.com/riverqueue/river v0.16.0 h1:YyQrs0kGgjuABwgat02DPUYS0TMyG2ZFlzvf6+fSFaw= -github.com/riverqueue/river v0.16.0/go.mod h1:pEZ8Gc15XyFjVY89nJeL256ub5z18XF7ukYn8ktqQrs= -github.com/riverqueue/river/riverdriver v0.16.0 h1:y4Df4e1Xk3Id0nnu1VxHJn9118OzmRHcmvOxM/i1Q30= -github.com/riverqueue/river/riverdriver v0.16.0/go.mod h1:7Kdf5HQDrLyLUUqPqXobaK+7zbcMctWeAl7yhg4nHes= -github.com/riverqueue/river/riverdriver/riverdatabasesql v0.16.0 h1:T/DcMmZXiJAyLN3CSyAoNcf3U4oAD9Ht/8Vd5SXv5YU= -github.com/riverqueue/river/riverdriver/riverdatabasesql v0.16.0/go.mod h1:a9EUhD2yGsAeM9eWo+QrGGbL8LVWoGj2m8KEzm0xUxE= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.16.0 h1:6HP296OPN+3ORL9qG1f561pldB5eovkLzfkNIQmaTXI= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.16.0/go.mod h1:MAeBNoTQ+CD3nRvV9mF6iCBfsGJTxYHZeZSP4MYoeUE= -github.com/riverqueue/river/rivershared v0.16.0 h1:L1lQ3gMwdIsxA6yF0/PwAdsFP0T82yBD1V03q2GuJDU= -github.com/riverqueue/river/rivershared v0.16.0/go.mod h1:y5Xu8Shcp44DUNnEQV4c6oWH4m2OTkSMCe6nRrgzT34= -github.com/riverqueue/river/rivertype v0.16.0 h1:iDjNtCiUbXwLraqNEyQdH/OD80f1wTo8Ai6WHYCwRxs= -github.com/riverqueue/river/rivertype v0.16.0/go.mod h1:DETcejveWlq6bAb8tHkbgJqmXWVLiFhTiEm8j7co1bE= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= +github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= +github.com/riverqueue/river v0.19.0 h1:WRh/NXhp+WEEY0HpCYgr4wSRllugYBt30HtyQ3jlz08= +github.com/riverqueue/river v0.19.0/go.mod h1:YJ7LA2uBdqFHQJzKyYc+X6S04KJeiwsS1yU5a1rynlk= +github.com/riverqueue/river/riverdriver v0.19.0 h1:NyHz5DfB13paT2lvaO0CKmwy4SFLbA7n6MFRGRtwii4= +github.com/riverqueue/river/riverdriver v0.19.0/go.mod h1:Soxi08hHkEvopExAp6ADG2437r4coSiB4QpuIL5E28k= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.19.0 h1:ytdPnueiv7ANxJcntBtYenrYZZLY5P0mXoDV0l4WsLk= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.19.0/go.mod h1:5Fahb3n+m1V0RAb0JlOIpzimoTlkOgudMfxSSCTcmFk= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.19.0 h1:QWg7VTDDXbtTF6srr7Y1C888PiNzqv379yQuNSnH2hg= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.19.0/go.mod h1:uvF1YS+iSQavCIHtaB/Y6O8A6Dnn38ctVQCpCpmHDZE= +github.com/riverqueue/river/rivershared v0.19.0 h1:TZvFM6CC+QgwQQUMQ5Ueuhx25ptgqcKqZQGsdLJnFeE= +github.com/riverqueue/river/rivershared v0.19.0/go.mod h1:JAvmohuC5lounVk8e3zXZIs07Da3klzEeJo1qDQIbjw= +github.com/riverqueue/river/rivertype v0.19.0 h1:5rwgdh21pVcU9WjrHIIO9qC2dOMdRrrZ/HZZOE0JRyY= +github.com/riverqueue/river/rivertype v0.19.0/go.mod h1:DETcejveWlq6bAb8tHkbgJqmXWVLiFhTiEm8j7co1bE= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= @@ -705,24 +734,25 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sony/gobreaker/v2 v2.0.0 h1:23AaR4JQ65y4rz8JWMzgXw2gKOykZ/qfqYunll4OwJ4= -github.com/sony/gobreaker/v2 v2.0.0/go.mod h1:8JnRUz80DJ1/ne8M8v7nmTs2713i58nIt4s7XcGe/DI= +github.com/sony/gobreaker/v2 v2.1.0 h1:av2BnjtRmVPWBvy5gSFPytm1J8BmN5AGhq875FfGKDM= +github.com/sony/gobreaker/v2 v2.1.0/go.mod h1:dO3Q/nCzxZj6ICjH6J/gM0r4oAwBMVLY8YAQf+NTtUg= github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ= github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= +github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -730,7 +760,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -739,14 +768,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 h1:1SWXcTphBQjYGWRRxLFIAR1LVtQEj4eR7xPtyeOVM/c= -github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203/go.mod h1:0Xw5cYMOYpgaWs+OOSx41ugycl2qvKTi9tlMMcZhFyY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -762,8 +789,8 @@ github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= -github.com/twilio/twilio-go v1.22.2 h1:LUz6OTWKY4/oW4e+O2ah2JMq03gJvGu6bxaF0Y7l+Xc= -github.com/twilio/twilio-go v1.22.2/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= +github.com/twilio/twilio-go v1.24.1 h1:bpBL1j5GRdJGSG+tCdo0O94BwK4uDOHQuNT5ndzljPg= +github.com/twilio/twilio-go v1.24.1/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -771,6 +798,8 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= @@ -783,47 +812,53 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= -github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= -github.com/zitadel/oidc/v3 v3.32.0 h1:Mw0EPZRC6h+OXAuT0Uk2BZIjJQNHLqUpaJCm6c3IByc= -github.com/zitadel/oidc/v3 v3.32.0/go.mod h1:DyE/XClysRK/ozFaZSqlYamKVnTh4l6Ln25ihSNI03w= -github.com/zitadel/passwap v0.6.0 h1:m9F3epFC0VkBXu25rihSLGyHvWiNlCzU5kk8RoI+SXQ= -github.com/zitadel/passwap v0.6.0/go.mod h1:kqAiJ4I4eZvm3Y6oAk6hlEqlZZOkjMHraGXF90GG7LI= -github.com/zitadel/saml v0.3.3 h1:Cn+1ZNeWlzMM7wxUxJfgNjXSW+Yu6UD4zWbpySA5GQM= -github.com/zitadel/saml v0.3.3/go.mod h1:QqKcguOt7mMVI6tkEfpkyzwnYRdlmn3kYQj3VTPUw1g= -github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= -github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= +github.com/zitadel/exifremove v0.1.0 h1:qD50ezWsfeeqfcvs79QyyjVfK+snN12v0U0deaU8aKg= +github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek= +github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= +github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= +github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf0= +github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs= +github.com/zitadel/passwap v0.7.0 h1:TQTr9TV75PLATGICor1g5hZDRNHRvB9t0Hn4XkiR7xQ= +github.com/zitadel/passwap v0.7.0/go.mod h1:/NakQNYahdU+YFEitVD6mlm8BLfkiIT+IM5wgClRoAY= +github.com/zitadel/saml v0.3.5 h1:L1RKWS5y66cGepVxUGjx/WSBOtrtSpRA/J3nn5BJLOY= +github.com/zitadel/saml v0.3.5/go.mod h1:ybs3e4tIWdYgSYBpuCsvf3T4FNDfbXYM+GPv5vIpHYk= +github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= +github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= -go.opentelemetry.io/otel/exporters/prometheus v0.50.0 h1:2Ewsda6hejmbhGFyUvWZjUThC98Cf8Zy6g0zkIimOng= -go.opentelemetry.io/otel/exporters/prometheus v0.50.0/go.mod h1:pMm5PkUo5YwbLiuEf7t2xg4wbP0/eSJrMxIMxKosynY= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0 h1:X3ZjNp36/WlkSYx0ul2jw4PtbNEDDeLskw3VPsrpYM0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0/go.mod h1:2uL/xnOXh0CHOBFCWXz5u1A4GXLiW+0IQIzVbeOEQ0U= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= -go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= +go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= +go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -853,15 +888,17 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -879,6 +916,9 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -909,7 +949,6 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -917,15 +956,17 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -936,8 +977,12 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -979,16 +1024,21 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -996,13 +1046,16 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1025,13 +1078,15 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo= -google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk= +google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc= +google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1044,12 +1099,12 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d h1:PksQg4dV6Sem3/HkBX+Ltq8T0ke0PKIRBNBatoDTVls= -google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:s7iA721uChleev562UJO2OYB0PPT9CMFjV+Ce7VJH5M= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1064,9 +1119,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1077,8 +1131,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1090,8 +1144,6 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= diff --git a/internal/actions/fields.go b/internal/actions/fields.go index de668b82b7..7d0919497e 100644 --- a/internal/actions/fields.go +++ b/internal/actions/fields.go @@ -62,10 +62,12 @@ func (f *FieldConfig) set(name string, value interface{}) { logging.WithFields("name", name).Error("tried to overwrite field") panic("tried to overwrite field") } - v, ok := value.(func(*FieldConfig) interface{}) - if ok { + switch v := value.(type) { + case func(config *FieldConfig) interface{}: f.fields[name] = v(f) - return + case func(config *FieldConfig) func(call goja.FunctionCall) goja.Value: + f.fields[name] = v(f) + default: + f.fields[name] = value } - f.fields[name] = value } diff --git a/internal/actions/http_module.go b/internal/actions/http_module.go index 2f9d09932c..db7253428d 100644 --- a/internal/actions/http_module.go +++ b/internal/actions/http_module.go @@ -176,16 +176,16 @@ type transport struct { } func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { - if httpConfig == nil { + if httpConfig == nil || len(httpConfig.DenyList) == 0 { return http.DefaultTransport.RoundTrip(req) } - if t.isHostBlocked(httpConfig.DenyList, req.URL) { - return nil, zerrors.ThrowInvalidArgument(nil, "ACTIO-N72d0", "host is denied") + if err := t.isHostBlocked(httpConfig.DenyList, req.URL); err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "ACTIO-N72d0", "host is denied") } return http.DefaultTransport.RoundTrip(req) } -func (t *transport) isHostBlocked(denyList []AddressChecker, address *url.URL) bool { +func (t *transport) isHostBlocked(denyList []AddressChecker, address *url.URL) error { host := address.Hostname() ip := net.ParseIP(host) ips := []net.IP{ip} @@ -194,17 +194,17 @@ func (t *transport) isHostBlocked(denyList []AddressChecker, address *url.URL) b var err error ips, err = t.lookup(host) if err != nil { - return true + return zerrors.ThrowInternal(err, "ACTIO-4m9s2", "lookup failed") } } - for _, blocked := range denyList { - if blocked.Matches(ips, host) { - return true + for _, denied := range denyList { + if err := denied.IsDenied(ips, host); err != nil { + return err } } - return false + return nil } type AddressChecker interface { - Matches([]net.IP, string) bool + IsDenied([]net.IP, string) error } diff --git a/internal/actions/http_module_config.go b/internal/actions/http_module_config.go index d1b965814e..eaab9e754e 100644 --- a/internal/actions/http_module_config.go +++ b/internal/actions/http_module_config.go @@ -1,6 +1,8 @@ package actions import ( + "errors" + "fmt" "net" "reflect" "strings" @@ -60,6 +62,9 @@ func HTTPConfigDecodeHook(from, to reflect.Value) (interface{}, error) { } func NewHostChecker(entry string) (AddressChecker, error) { + if entry == "" { + return nil, nil + } _, network, err := net.ParseCIDR(entry) if err == nil { return &HostChecker{Net: network}, nil @@ -76,19 +81,39 @@ type HostChecker struct { Domain string } -func (c *HostChecker) Matches(ips []net.IP, address string) bool { +type AddressDeniedError struct { + deniedBy string +} + +func NewAddressDeniedError(deniedBy string) *AddressDeniedError { + return &AddressDeniedError{deniedBy: deniedBy} +} + +func (e *AddressDeniedError) Error() string { + return fmt.Sprintf("address is denied by '%s'", e.deniedBy) +} + +func (e *AddressDeniedError) Is(target error) bool { + var addressDeniedErr *AddressDeniedError + if !errors.As(target, &addressDeniedErr) { + return false + } + return e.deniedBy == addressDeniedErr.deniedBy +} + +func (c *HostChecker) IsDenied(ips []net.IP, address string) error { // if the address matches the domain, no additional checks as needed if c.Domain == address { - return true + return NewAddressDeniedError(c.Domain) } // otherwise we need to check on ips (incl. the resolved ips of the host) for _, ip := range ips { if c.Net != nil && c.Net.Contains(ip) { - return true + return NewAddressDeniedError(c.Net.String()) } if c.IP != nil && c.IP.Equal(ip) { - return true + return NewAddressDeniedError(c.IP.String()) } } - return false + return nil } diff --git a/internal/actions/http_module_test.go b/internal/actions/http_module_test.go index 7a1f8d7816..50a007feeb 100644 --- a/internal/actions/http_module_test.go +++ b/internal/actions/http_module_test.go @@ -3,6 +3,7 @@ package actions import ( "bytes" "context" + "errors" "io" "net" "net/http" @@ -11,6 +12,7 @@ import ( "testing" "github.com/dop251/goja" + "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/record" @@ -34,21 +36,21 @@ func Test_isHostBlocked(t *testing.T) { name string fields fields args args - want bool + want error }{ { name: "in range", args: args{ address: mustNewURL(t, "https://192.168.5.4/hodor"), }, - want: true, + want: NewAddressDeniedError("192.168.5.0/24"), }, { name: "exact ip", args: args{ address: mustNewURL(t, "http://127.0.0.1:8080/hodor"), }, - want: true, + want: NewAddressDeniedError("127.0.0.1"), }, { name: "address match", @@ -60,7 +62,7 @@ func Test_isHostBlocked(t *testing.T) { args: args{ address: mustNewURL(t, "https://test.com:42/hodor"), }, - want: true, + want: NewAddressDeniedError("test.com"), }, { name: "address not match", @@ -72,7 +74,7 @@ func Test_isHostBlocked(t *testing.T) { args: args{ address: mustNewURL(t, "https://test2.com/hodor"), }, - want: false, + want: nil, }, { name: "looked up ip matches", @@ -84,7 +86,19 @@ func Test_isHostBlocked(t *testing.T) { args: args{ address: mustNewURL(t, "https://test2.com/hodor"), }, - want: true, + want: NewAddressDeniedError("127.0.0.1"), + }, + { + name: "looked up failure", + fields: fields{ + lookup: func(host string) ([]net.IP, error) { + return nil, errors.New("some error") + }, + }, + args: args{ + address: mustNewURL(t, "https://test2.com/hodor"), + }, + want: zerrors.ThrowInternal(nil, "ACTIO-4m9s2", "lookup failed"), }, } for _, tt := range tests { @@ -92,9 +106,8 @@ func Test_isHostBlocked(t *testing.T) { trans := &transport{ lookup: tt.fields.lookup, } - if got := trans.isHostBlocked(denyList, tt.args.address); got != tt.want { - t.Errorf("isHostBlocked() = %v, want %v", got, tt.want) - } + got := trans.isHostBlocked(denyList, tt.args.address) + assert.ErrorIs(t, got, tt.want) }) } } diff --git a/internal/actions/object/auth_request.go b/internal/actions/object/auth_request.go index 7d4d869af1..93d842f98f 100644 --- a/internal/actions/object/auth_request.go +++ b/internal/actions/object/auth_request.go @@ -18,6 +18,9 @@ func AuthRequestField(authRequest *domain.AuthRequest) func(c *actions.FieldConf } func AuthRequestFromDomain(c *actions.FieldConfig, request *domain.AuthRequest) goja.Value { + if request == nil { + return c.Runtime.ToValue(nil) + } var maxAuthAge *time.Duration if request.MaxAuthAge != nil { maxAuthAgeCopy := *request.MaxAuthAge diff --git a/internal/actions/object/user_grant.go b/internal/actions/object/user_grant.go index efa8f9cc66..2cc693c202 100644 --- a/internal/actions/object/user_grant.go +++ b/internal/actions/object/user_grant.go @@ -158,9 +158,9 @@ func UserGrantsToDomain(userID string, actionUserGrants []UserGrant) []*domain.U func mapObjectToGrant(object *goja.Object, grant *UserGrant) { for _, key := range object.Keys() { switch key { - case "projectId": + case "projectId", "projectID": grant.ProjectID = object.Get(key).String() - case "projectGrantId": + case "projectGrantId", "projectGrantID": grant.ProjectGrantID = object.Get(key).String() case "roles": if roles, ok := object.Get(key).Export().([]interface{}); ok { diff --git a/internal/api/api.go b/internal/api/api.go index 15d6c5b996..62d3e14b35 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -15,7 +15,7 @@ import ( healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" http_util "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" @@ -29,7 +29,7 @@ import ( type API struct { port uint16 grpcServer *grpc.Server - verifier internal_authz.APITokenVerifier + verifier authz.APITokenVerifier health healthCheck router *mux.Router hostHeaders []string @@ -72,8 +72,9 @@ func New( port uint16, router *mux.Router, queries *query.Queries, - verifier internal_authz.APITokenVerifier, - authZ internal_authz.Config, + verifier authz.APITokenVerifier, + systemAuthz authz.Config, + authZ authz.Config, tlsConfig *tls.Config, externalDomain string, hostHeaders []string, @@ -89,7 +90,7 @@ func New( hostHeaders: hostHeaders, } - api.grpcServer = server.CreateServer(api.verifier, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) + api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig) if err != nil { return nil, err diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 57ad3710bc..8c3c51c6aa 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -94,13 +94,13 @@ func DefaultErrorHandler(translator *i18n.Translator) func(w http.ResponseWriter } } -func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler { +func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, systemAuthCOnfig authz.Config, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler { translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") h := &Handler{ commands: commands, errorHandler: DefaultErrorHandler(translator), - authInterceptor: http_mw.AuthorizationInterceptor(verifier, authConfig), + authInterceptor: http_mw.AuthorizationInterceptor(verifier, systemAuthCOnfig, authConfig), idGenerator: idGenerator, storage: storage, query: queries, @@ -129,8 +129,10 @@ func (l *publicFileDownloader) ResourceOwner(_ context.Context, ownerPath string return ownerPath } -const maxMemory = 2 << 20 -const paramFile = "file" +const ( + maxMemory = 2 << 20 + paramFile = "file" +) func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index 2099b3e426..25130584a0 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "slices" "strings" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -16,14 +17,14 @@ const ( // CheckUserAuthorization verifies that: // - the token is active, -// - the organisation (**either** provided by ID or verified domain) exists +// - the organization (**either** provided by ID or verified domain) exists // - the user is permitted to call the requested endpoint (permission option in proto) // it will pass the [CtxData] and permission of the user into the ctx [context.Context] -func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { +func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, systemRolePermissionMapping []RoleMapping, rolePermissionMapping []RoleMapping, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { ctx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() - ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier) + ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, systemRolePermissionMapping) if err != nil { return nil, err } @@ -34,7 +35,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, }, nil } - requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID) + requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, systemRolePermissionMapping, rolePermissionMapping, ctxData, ctxData.OrgID) if err != nil { return nil, err } @@ -125,3 +126,33 @@ func GetAllPermissionCtxIDs(perms []string) []string { } return ctxIDs } + +type SystemUserPermissions struct { + MemberType MemberType `json:"member_type"` + AggregateID string `json:"aggregate_id"` + ObjectID string `json:"object_id"` + Permissions []string `json:"permissions"` +} + +// systemMembershipsToUserPermissions converts system memberships based on roles, +// to SystemUserPermissions, using the passed role mapping. +func systemMembershipsToUserPermissions(memberships Memberships, roleMap []RoleMapping) []SystemUserPermissions { + if memberships == nil { + return nil + } + systemUserPermissions := make([]SystemUserPermissions, len(memberships)) + for i, systemPerm := range memberships { + permissions := make([]string, 0, len(systemPerm.Roles)) + for _, role := range systemPerm.Roles { + permissions = append(permissions, getPermissionsFromRole(roleMap, role)...) + } + slices.Sort(permissions) + permissions = slices.Compact(permissions) // remove duplicates + + systemUserPermissions[i].MemberType = systemPerm.MemberType + systemUserPermissions[i].AggregateID = systemPerm.AggregateID + systemUserPermissions[i].ObjectID = systemPerm.ObjectID + systemUserPermissions[i].Permissions = permissions + } + return systemUserPermissions +} diff --git a/internal/api/authz/authorization_test.go b/internal/api/authz/authorization_test.go index 4b81c73d81..af49dcc5c6 100644 --- a/internal/api/authz/authorization_test.go +++ b/internal/api/authz/authorization_test.go @@ -3,6 +3,8 @@ package authz import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/zitadel/zitadel/internal/zerrors" ) @@ -276,3 +278,127 @@ func Test_GetPermissionCtxIDs(t *testing.T) { }) } } + +func Test_systemMembershipsToUserPermissions(t *testing.T) { + roleMap := []RoleMapping{ + { + Role: "FOO_BAR", + Permissions: []string{"foo.bar.read", "foo.bar.write"}, + }, + { + Role: "BAR_FOO", + Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read"}, + }, + } + + type args struct { + memberships Memberships + roleMap []RoleMapping + } + tests := []struct { + name string + args args + want []SystemUserPermissions + }{ + { + name: "nil memberships", + args: args{ + memberships: nil, + roleMap: roleMap, + }, + want: nil, + }, + { + name: "empty memberships", + args: args{ + memberships: Memberships{}, + roleMap: roleMap, + }, + want: []SystemUserPermissions{}, + }, + { + name: "single membership", + args: args{ + memberships: Memberships{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Roles: []string{"FOO_BAR"}, + }, + }, + roleMap: roleMap, + }, + want: []SystemUserPermissions{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Permissions: []string{"foo.bar.read", "foo.bar.write"}, + }, + }, + }, + { + name: "multiple memberships", + args: args{ + memberships: Memberships{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Roles: []string{"FOO_BAR"}, + }, + { + MemberType: MemberTypeIAM, + AggregateID: "1", + ObjectID: "2", + Roles: []string{"BAR_FOO"}, + }, + }, + roleMap: roleMap, + }, + want: []SystemUserPermissions{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Permissions: []string{"foo.bar.read", "foo.bar.write"}, + }, + { + MemberType: MemberTypeIAM, + AggregateID: "1", + ObjectID: "2", + Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read"}, + }, + }, + }, + { + name: "multiple roles", + args: args{ + memberships: Memberships{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Roles: []string{"FOO_BAR", "BAR_FOO"}, + }, + }, + roleMap: roleMap, + }, + want: []SystemUserPermissions{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read", "foo.bar.write"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := systemMembershipsToUserPermissions(tt.args.memberships, tt.args.roleMap) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/authz/context.go b/internal/api/authz/context.go index ff401f8862..ff2fa8d445 100644 --- a/internal/api/authz/context.go +++ b/internal/api/authz/context.go @@ -1,4 +1,4 @@ -//go:generate enumer -type MemberType -trimprefix MemberType +//go:generate enumer -type MemberType -trimprefix MemberType -json -sql package authz @@ -25,13 +25,14 @@ const ( ) type CtxData struct { - UserID string - OrgID string - ProjectID string - AgentID string - PreferredLanguage string - ResourceOwner string - SystemMemberships Memberships + UserID string + OrgID string + ProjectID string + AgentID string + PreferredLanguage string + ResourceOwner string + SystemMemberships Memberships + SystemUserPermissions []SystemUserPermissions } func (ctxData CtxData) IsZero() bool { @@ -50,7 +51,8 @@ type Memberships []*Membership type Membership struct { MemberType MemberType AggregateID string - //ObjectID differs from aggregate id if object is sub of an aggregate + InstanceID string + // ObjectID differs from aggregate id if object is sub of an aggregate ObjectID string Roles []string @@ -96,7 +98,7 @@ func (s SystemTokenVerifierFunc) VerifySystemToken(ctx context.Context, token st return s(ctx, token, orgID) } -func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier) (_ CtxData, err error) { +func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier, systemRoleMap []RoleMapping) (_ CtxData, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() tokenWOBearer, err := extractBearerToken(token) @@ -131,13 +133,14 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain st } } return CtxData{ - UserID: userID, - OrgID: orgID, - ProjectID: projectID, - AgentID: agentID, - PreferredLanguage: prefLang, - ResourceOwner: resourceOwner, - SystemMemberships: sysMemberships, + UserID: userID, + OrgID: orgID, + ProjectID: projectID, + AgentID: agentID, + PreferredLanguage: prefLang, + ResourceOwner: resourceOwner, + SystemMemberships: sysMemberships, + SystemUserPermissions: systemMembershipsToUserPermissions(sysMemberships, systemRoleMap), }, nil } diff --git a/internal/api/authz/membertype_enumer.go b/internal/api/authz/membertype_enumer.go index 5de4c92292..9354194660 100644 --- a/internal/api/authz/membertype_enumer.go +++ b/internal/api/authz/membertype_enumer.go @@ -1,8 +1,10 @@ -// Code generated by "enumer -type MemberType -trimprefix MemberType"; DO NOT EDIT. +// Code generated by "enumer -type MemberType -trimprefix MemberType -json -sql"; DO NOT EDIT. package authz import ( + "database/sql/driver" + "encoding/json" "fmt" "strings" ) @@ -92,3 +94,50 @@ func (i MemberType) IsAMemberType() bool { } return false } + +// MarshalJSON implements the json.Marshaler interface for MemberType +func (i MemberType) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for MemberType +func (i *MemberType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("MemberType should be a string, got %s", data) + } + + var err error + *i, err = MemberTypeString(s) + return err +} + +func (i MemberType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *MemberType) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of MemberType: %[1]T(%[1]v)", value) + } + + val, err := MemberTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/internal/api/authz/permissions.go b/internal/api/authz/permissions.go index e96a7b256b..904fbbc33a 100644 --- a/internal/api/authz/permissions.go +++ b/internal/api/authz/permissions.go @@ -7,8 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { - requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID) +func CheckPermission(ctx context.Context, resolver MembershipsResolver, systemUserRoleMapping []RoleMapping, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { + requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, systemUserRoleMapping, roleMappings, GetCtxData(ctx), orgID) if err != nil { return err } @@ -22,7 +22,7 @@ func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMapp // getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level), // and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately. -func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { +func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, systemUserRoleMappings []RoleMapping, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -31,7 +31,7 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi } if ctxData.SystemMemberships != nil { - requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings) + requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, systemUserRoleMappings) return requestedPermissions, allPermissions, nil } diff --git a/internal/api/authz/permissions_test.go b/internal/api/authz/permissions_test.go index 7919747de6..93243d0c09 100644 --- a/internal/api/authz/permissions_test.go +++ b/internal/api/authz/permissions_test.go @@ -120,7 +120,7 @@ func Test_GetUserPermissions(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID) + _, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, nil, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID) if tt.wantErr && err == nil { t.Errorf("got wrong result, should get err: actual: %v ", err) diff --git a/internal/api/grpc/resources/action/v3alpha/execution.go b/internal/api/grpc/action/v2beta/execution.go similarity index 59% rename from internal/api/grpc/resources/action/v3alpha/execution.go rename to internal/api/grpc/action/v2beta/execution.go index 94ad17c2f0..5477a8128e 100644 --- a/internal/api/grpc/resources/action/v3alpha/execution.go +++ b/internal/api/grpc/action/v2beta/execution.go @@ -3,33 +3,21 @@ package action import ( "context" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/api/authz" - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - reqTargets := req.GetExecution().GetTargets() + reqTargets := req.GetTargets() targets := make([]*execution.Target, len(reqTargets)) for i, target := range reqTargets { - switch t := target.GetType().(type) { - case *action.ExecutionTargetType_Include: - include, err := conditionToInclude(t.Include) - if err != nil { - return nil, err - } - targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeInclude, Target: include} - case *action.ExecutionTargetType_Target: - targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: t.Target} - } + targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: target} } set := &command.SetExecution{ Targets: targets, @@ -56,63 +44,23 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque return nil, err } return &action.SetExecutionResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + SetDate: timestamppb.New(details.EventDate), }, nil } -func conditionToInclude(cond *action.Condition) (string, error) { - switch t := cond.GetConditionType().(type) { - case *action.Condition_Request: - cond := executionConditionFromRequest(t.Request) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(domain.ExecutionTypeRequest), nil - case *action.Condition_Response: - cond := executionConditionFromResponse(t.Response) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(domain.ExecutionTypeRequest), nil - case *action.Condition_Event: - cond := executionConditionFromEvent(t.Event) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(), nil - case *action.Condition_Function: - cond := command.ExecutionFunctionCondition(t.Function.GetName()) - if err := cond.IsValid(); err != nil { - return "", err - } - return cond.ID(), nil - default: - return "", zerrors.ThrowInvalidArgument(nil, "ACTION-9BBob", "Errors.Execution.ConditionInvalid") - } -} - func (s *Server) ListExecutionFunctions(ctx context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionFunctionsResponse{ Functions: s.ListActionFunctions(), }, nil } func (s *Server) ListExecutionMethods(ctx context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionMethodsResponse{ Methods: s.ListGRPCMethods(), }, nil } func (s *Server) ListExecutionServices(ctx context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } return &action.ListExecutionServicesResponse{ Services: s.ListGRPCServices(), }, nil diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go new file mode 100644 index 0000000000..0c5018dbb6 --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go @@ -0,0 +1,1309 @@ +//go:build integration + +package action_test + +import ( + "context" + "encoding/base64" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" + oidc_api "github.com/zitadel/zitadel/internal/api/oidc" + saml_api "github.com/zitadel/zitadel/internal/api/saml" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/internal/query" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/app" + "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/metadata" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" + saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +const ( + redirectURIImplicit = "http://localhost:9999/callback" +) + +var ( + loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}} +) + +func TestServer_ExecutionTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + fullMethod := action.ActionService_GetTarget_FullMethodName + + tests := []struct { + name string + ctx context.Context + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (closeF func(), calledF func() bool) + clean func(context.Context) + req *action.GetTargetRequest + want *action.GetTargetResponse + wantErr bool + }{ + { + name: "GetTarget, request and response, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // create target for target changes + targetCreatedName := gofakeit.Name() + targetCreatedURL := "https://nonexistent" + + targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}} + changedRequest := &action.GetTargetRequest{Id: targetCreated.GetId()} + // replace original request with different targetID + urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusOK, changedRequest) + + targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, false) + + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()}) + + // expected response from the GetTarget + expectedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: targetCreated.GetId(), + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + + changedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: "changed", + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + // content for update + response.Target = &action.Target{ + Id: "changed", + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instance.ID(), + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: middleware.Message{Message: changedRequest}, + Response: middleware.Message{Message: expectedResponse}, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusOK, changedResponse) + + targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, false) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()}) + return func() { + closeRequest() + closeResponse() + }, func() bool { + if calledRequest() != 1 { + return false + } + if calledResponse() != 1 { + return false + } + return true + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{ + Id: "something", + }, + want: &action.GetTargetResponse{ + // defined in the dependency function + }, + }, + { + name: "GetTarget, request, interrupt", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}} + urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusInternalServerError, nil) + + targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()}) + // GetTarget with used target + request.Id = targetRequest.GetId() + return func() { + closeRequest() + }, func() bool { + return calledRequest() == 1 + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{}, + wantErr: true, + }, + { + name: "GetTarget, response, interrupt", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // create target for target changes + targetCreatedName := gofakeit.Name() + targetCreatedURL := "https://nonexistent" + + targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // GetTarget with used target + request.Id = targetCreated.GetId() + + // expected response from the GetTarget + expectedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: targetCreated.GetId(), + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instance.ID(), + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: middleware.Message{Message: request}, + Response: middleware.Message{Message: expectedResponse}, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusInternalServerError, nil) + + targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()}) + return func() { + closeResponse() + }, func() bool { + return calledResponse() == 1 + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + closeF, calledF := tt.dep(tt.ctx, tt.req, tt.want) + defer closeF() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2beta.GetTarget(tt.ctx, tt.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + require.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want.GetTarget(), got.GetTarget()) + + }, retryDuration, tick, "timeout waiting for expected execution result") + + if tt.clean != nil { + tt.clean(tt.ctx) + } + require.True(t, calledF()) + }) + } +} + +func TestServer_ExecutionTarget_Event(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 0, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 50 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 50, + expectedCalls: 50, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + // call takes longer than timeout of target + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 5*time.Second, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, error logs", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, error logs", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 1*time.Second, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 20 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 20, + expectedCalls: 20, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []string) { + instance.SetExecution(ctx, t, condition, targets) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2beta.ListExecutions(ctx, &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{ + {Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{Conditions: []*action.Condition{condition}}, + }}, + }, + }) + if !assert.NoError(ttt, err) { + return + } + if !assert.Len(ttt, got.GetResult(), 1) { + return + } + gotTargets := got.GetResult()[0].GetTargets() + // always first check length, otherwise its failed anyway + if assert.Len(ttt, gotTargets, len(targets)) { + for i := range targets { + assert.EqualExportedValues(ttt, targets[i], gotTargets[i]) + } + } + }, retryDuration, tick, "timeout waiting for expected execution result") + return +} + +func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { + resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2beta.ListTargets(ctx, &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{ + {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetId()}}, + }}, + }, + }) + if !assert.NoError(ttt, err) { + return + } + if !assert.Len(ttt, got.GetResult(), 1) { + return + } + config := got.GetResult()[0] + assert.Equal(ttt, config.GetEndpoint(), endpoint) + switch ty { + case domain.TargetTypeWebhook: + if !assert.NotNil(ttt, config.GetRestWebhook()) { + return + } + assert.Equal(ttt, interrupt, config.GetRestWebhook().GetInterruptOnError()) + case domain.TargetTypeAsync: + assert.NotNil(ttt, config.GetRestAsync()) + case domain.TargetTypeCall: + if !assert.NotNil(ttt, config.GetRestCall()) { + return + } + assert.Equal(ttt, interrupt, config.GetRestCall().GetInterruptOnError()) + } + }, retryDuration, tick, "timeout waiting for expected execution result") + return resp +} + +func conditionRequestFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func conditionResponseFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func conditionEvent(event string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: event, + }, + }, + }, + } +} + +func conditionFunction(function string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{ + Name: function, + }, + }, + } +} + +func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + require.NoError(t, err) + + type want struct { + addedClaims map[string]any + addedLogClaims map[string][]string + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) + req *oidc_pb.CreateCallbackRequest + want want + wantErr bool + }{ + { + name: "append claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added", Value: "value"}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: map[string]any{ + "added": "value", + }, + }, + wantErr: false, + }, + { + name: "append log claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendLogClaims: []string{ + "addedLog", + }, + } + return expectPreUserinfoExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedLogClaims: map[string][]string{ + "urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog"}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "full usage", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + AppendLogClaims: []string{ + "addedLog1", + "addedLog2", + "addedLog3", + }, + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value1"}, + {Key: "added2", Value: "value2"}, + {Key: "added3", Value: "value3"}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: map[string]any{ + "added1": "value1", + "added2": "value2", + "added3": "value3", + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + addedLogClaims: map[string][]string{ + "urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog1", "addedLog2", "addedLog3"}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1)) + require.NoError(t, err) + claims := getIDTokenClaimsFromCallbackURL(tt.ctx, t, instance, client.GetClientId(), callbackUrl) + + for k, v := range tt.want.addedClaims { + value, ok := claims[k] + if !assert.True(t, ok) { + return + } + assert.Equal(t, v, value) + } + for k, v := range tt.want.addedLogClaims { + value, ok := claims[k] + if !assert.True(t, ok) { + return + } + assert.ElementsMatch(t, v, value) + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + }) + } +} + +func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.CallbackKind = &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), []string{targetResp.GetId()}) + return userResp.GetUserId(), closeF +} + +func createSession(ctx context.Context, t *testing.T, instance *integration.Instance, userID string) *session.CreateSessionResponse { + sessionResp, err := instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: userID, + }, + }, + }, + }) + require.NoError(t, err) + return sessionResp +} + +func checkForSetMetadata(ctx context.Context, t *testing.T, instance *integration.Instance, userID string, metadataExpected []*metadata.Metadata) { + integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + assert.EventuallyWithT(t, func(ct *assert.CollectT) { + metadataResp, err := instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{Id: userID}) + if !assert.NoError(ct, err) { + return + } + for _, dataExpected := range metadataExpected { + found := false + for _, dataCheck := range metadataResp.GetResult() { + if dataExpected.Key == dataCheck.Key { + found = true + if !assert.Equal(ct, dataExpected.Value, dataCheck.Value) { + return + } + } + } + if !assert.True(ct, found) { + return + } + } + }, retryDuration, tick) +} + +func getIDTokenClaimsFromCallbackURL(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, callbackURL *url.URL) map[string]any { + accessToken := callbackURL.Query().Get("access_token") + idToken := callbackURL.Query().Get("id_token") + + provider, err := instance.CreateRelyingParty(ctx, clientID, redirectURIImplicit, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone) + require.NoError(t, err) + claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier()) + require.NoError(t, err) + return claims.Claims +} + +type CustomAccessTokenClaims struct { + oidc.TokenClaims + Added1 string `json:"added1,omitempty"` + Added2 string `json:"added2,omitempty"` + Added3 string `json:"added3,omitempty"` + Log []string `json:"urn:zitadel:iam:action:function/preaccesstoken:log,omitempty"` +} + +func getAccessTokenClaims(ctx context.Context, t *testing.T, instance *integration.Instance, callbackURL *url.URL) *CustomAccessTokenClaims { + accessToken := callbackURL.Query().Get("access_token") + + verifier := op.NewAccessTokenVerifier(instance.OIDCIssuer(), rp.NewRemoteKeySet(http.DefaultClient, instance.OIDCIssuer()+"/oauth/v2/keys")) + + claims, err := op.VerifyAccessToken[*CustomAccessTokenClaims](ctx, accessToken, verifier) + require.NoError(t, err) + return claims +} + +func contextInfoForUserOIDC(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfo { + return &oidc_api.ContextInfo{ + Function: function, + UserInfo: &oidc.UserInfo{ + Subject: userResp.GetUserId(), + }, + User: &query.User{ + ID: userResp.GetUserId(), + CreationDate: userResp.Details.ChangeDate.AsTime(), + ChangeDate: userResp.Details.ChangeDate.AsTime(), + ResourceOwner: instance.DefaultOrg.GetId(), + Sequence: userResp.Details.Sequence, + State: 1, + Username: email, + PreferredLoginName: email, + Human: &query.Human{ + FirstName: "Mickey", + LastName: "Mouse", + NickName: "Mickey", + DisplayName: "Mickey Mouse", + AvatarKey: "", + PreferredLanguage: language.Dutch, + Gender: 2, + Email: domain.EmailAddress(email), + IsEmailVerified: true, + Phone: domain.PhoneNumber(phone), + IsPhoneVerified: true, + PasswordChangeRequired: false, + PasswordChanged: time.Time{}, + MFAInitSkipped: time.Time{}, + }, + }, + UserMetadata: nil, + Org: &query.UserInfoOrg{ + ID: instance.DefaultOrg.GetId(), + Name: instance.DefaultOrg.GetName(), + PrimaryDomain: instance.DefaultOrg.GetPrimaryDomain(), + }, + UserGrants: nil, + Response: nil, + } +} + +func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + require.NoError(t, err) + + type want struct { + addedClaims *CustomAccessTokenClaims + addedLogClaims map[string][]string + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) + req *oidc_pb.CreateCallbackRequest + want want + wantErr bool + }{ + { + name: "append claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value"}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Added1: "value", + }, + }, + wantErr: false, + }, + { + name: "append log claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendLogClaims: []string{ + "addedLog", + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Log: []string{"addedLog"}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "full usage", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + AppendLogClaims: []string{ + "addedLog1", + "addedLog2", + "addedLog3", + }, + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value1"}, + {Key: "added2", Value: "value2"}, + {Key: "added3", Value: "value3"}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Added1: "value1", + Added2: "value2", + Added3: "value3", + Log: []string{"addedLog1", "addedLog2", "addedLog3"}, + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1)) + require.NoError(t, err) + claims := getAccessTokenClaims(tt.ctx, t, instance, callbackUrl) + + if tt.want.addedClaims != nil { + assert.Equal(t, tt.want.addedClaims.Added1, claims.Added1) + assert.Equal(t, tt.want.addedClaims.Added2, claims.Added2) + assert.Equal(t, tt.want.addedClaims.Added3, claims.Added3) + assert.Equal(t, tt.want.addedClaims.Log, claims.Log) + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + + }) + } +} + +func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.CallbackKind = &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), []string{targetResp.GetId()}) + return userResp.GetUserId(), closeF +} + +func TestServer_ExecutionTargetPreSAMLResponse(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + + idpMetadata, err := instance.GetSAMLIDPMetadata() + require.NoError(t, err) + + acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1] + _, _, spMiddlewarePost := createSAMLApplication(isolatedIAMCtx, t, instance, idpMetadata, saml.HTTPPostBinding, false, false) + + type want struct { + addedAttributes map[string][]saml.AttributeValue + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) + req *saml_pb.CreateResponseRequest + want want + wantErr bool + }{ + { + name: "append attribute", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + AppendAttribute: []*saml_api.AppendAttribute{ + {Name: "added", NameFormat: "format", Value: []string{"value"}}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + addedAttributes: map[string][]saml.AttributeValue{ + "added": {saml.AttributeValue{Value: "value"}}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + AppendAttribute: []*saml_api.AppendAttribute{ + {Name: "added1", NameFormat: "format", Value: []string{"value1"}}, + {Name: "added2", NameFormat: "format", Value: []string{"value2"}}, + {Name: "added3", NameFormat: "format", Value: []string{"value3"}}, + }, + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + addedAttributes: map[string][]saml.AttributeValue{ + "added1": {saml.AttributeValue{Value: "value1"}}, + "added2": {saml.AttributeValue{Value: "value2"}}, + "added3": {saml.AttributeValue{Value: "value3"}}, + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.SAMLv2.CreateResponse(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + attributes := getSAMLResponseAttributes(t, got.GetPost().GetSamlResponse(), spMiddlewarePost) + for k, v := range tt.want.addedAttributes { + found := false + for _, attribute := range attributes { + if attribute.Name == k { + found = true + assert.Equal(t, v, attribute.Values) + } + } + if !assert.True(t, found) { + return + } + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + }) + } +} + +func expectPreSAMLResponseExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *saml_pb.CreateResponseRequest, response *saml_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.ResponseKind = &saml_pb.CreateResponseRequest_Session{ + Session: &saml_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserSAML(instance, "function/presamlresponse", userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), []string{targetResp.GetId()}) + + return userResp.GetUserId(), closeF +} + +func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding string) (string, *samlsp.Middleware) { + rootURL := "example." + gofakeit.DomainName() + spMiddleware, err := integration.CreateSAMLSP("https://"+rootURL, idpMetadata, binding) + require.NoError(t, err) + return rootURL, spMiddleware +} + +func createSAMLApplication(ctx context.Context, t *testing.T, instance *integration.Instance, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) { + project, err := instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) + require.NoError(t, err) + rootURL, sp := createSAMLSP(t, idpMetadata, binding) + _, err = instance.CreateSAMLClient(ctx, project.GetId(), sp) + require.NoError(t, err) + return project.GetId(), rootURL, sp +} + +func getSAMLResponseAttributes(t *testing.T, samlResponse string, sp *samlsp.Middleware) []saml.Attribute { + data, err := base64.StdEncoding.DecodeString(samlResponse) + require.NoError(t, err) + sp.ServiceProvider.AllowIDPInitiated = true + assertion, err := sp.ServiceProvider.ParseXMLResponse(data, []string{}) + require.NoError(t, err) + return assertion.AttributeStatements[0].Attributes +} + +func contextInfoForUserSAML(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *saml_api.ContextInfo { + return &saml_api.ContextInfo{ + Function: function, + User: &query.User{ + ID: userResp.GetUserId(), + CreationDate: userResp.Details.ChangeDate.AsTime(), + ChangeDate: userResp.Details.ChangeDate.AsTime(), + ResourceOwner: instance.DefaultOrg.GetId(), + Sequence: userResp.Details.Sequence, + State: 1, + Type: domain.UserTypeHuman, + Username: email, + PreferredLoginName: email, + LoginNames: []string{email}, + Human: &query.Human{ + FirstName: "Mickey", + LastName: "Mouse", + NickName: "Mickey", + DisplayName: "Mickey Mouse", + AvatarKey: "", + PreferredLanguage: language.Dutch, + Gender: 2, + Email: domain.EmailAddress(email), + IsEmailVerified: true, + Phone: domain.PhoneNumber(phone), + IsPhoneVerified: true, + PasswordChangeRequired: false, + PasswordChanged: time.Time{}, + MFAInitSkipped: time.Time{}, + }, + }, + UserGrants: nil, + Response: nil, + } +} diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_test.go new file mode 100644 index 0000000000..2199b9f454 --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/execution_test.go @@ -0,0 +1,565 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" +) + +func TestServer_SetExecution_Request(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.NotExistingService/List", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "service, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We want to have the same response no matter how often we call the function + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, expectedSetDate bool, actualResp *action.SetExecutionResponse) { + if expectedSetDate { + if !setDate.IsZero() { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, setDate) + } else { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.SetDate) + } +} + +func TestServer_SetExecution_Response(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: "/zitadel.session.v2beta.NotExistingService/List", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "service, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Event(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_All{ + All: true, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "event, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: "user.human.notexisting", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "event, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: "user.human.added", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "group, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user.notexisting", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "group, level 1, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "group, level 2, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user.human", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Function(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "function, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{Name: "xxx"}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "function, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{Name: "presamlresponse"}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} diff --git a/internal/api/grpc/action/v2beta/integration_test/query_test.go b/internal/api/grpc/action/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..5c59bee5d1 --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/query_test.go @@ -0,0 +1,793 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" +) + +func TestServer_GetTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error + req *action.GetTargetRequest + } + tests := []struct { + name string + args args + want *action.GetTargetResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.GetTargetRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.GetTargetRequest{Id: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, async, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, webhook interruptOnError, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, call, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, call interruptOnError, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + require.NoError(t, err) + } + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, 2*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2beta.GetTarget(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err, "Error: "+err.Error()) + return + } + assert.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected target result") + }) + } +} + +func TestServer_ListTargets(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse) + req *action.ListTargetsRequest + } + tests := []struct { + name string + args args + want *action.ListTargetsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.ListTargetsRequest{}, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{ + {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Result: []*action.Target{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{resp.GetId()}, + }, + } + + response.Result[0].Id = resp.GetId() + response.Result[0].Name = name + response.Result[0].CreationDate = resp.GetCreationDate() + response.Result[0].ChangeDate = resp.GetCreationDate() + response.Result[0].SigningKey = resp.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, { + name: "list single name", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{ + TargetNameFilter: &action.TargetNameFilter{ + TargetName: name, + }, + } + + response.Result[0].Id = resp.GetId() + response.Result[0].Name = name + response.Result[0].CreationDate = resp.GetCreationDate() + response.Result[0].ChangeDate = resp.GetCreationDate() + response.Result[0].SigningKey = resp.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name1 := gofakeit.Name() + name2 := gofakeit.Name() + name3 := gofakeit.Name() + resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false) + resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true) + resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false) + request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Result[2].Id = resp1.GetId() + response.Result[2].Name = name1 + response.Result[2].CreationDate = resp1.GetCreationDate() + response.Result[2].ChangeDate = resp1.GetCreationDate() + response.Result[2].SigningKey = resp1.GetSigningKey() + + response.Result[1].Id = resp2.GetId() + response.Result[1].Name = name2 + response.Result[1].CreationDate = resp2.GetCreationDate() + response.Result[1].ChangeDate = resp2.GetCreationDate() + response.Result[1].SigningKey = resp2.GetSigningKey() + + response.Result[0].Id = resp3.GetId() + response.Result[0].Name = name3 + response.Result[0].CreationDate = resp3.GetCreationDate() + response.Result[0].ChangeDate = resp3.GetCreationDate() + response.Result[0].SigningKey = resp3.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Result: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ActionV2beta.ListTargets(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Result, len(tt.want.Result)) { + for i := range tt.want.Result { + assert.EqualExportedValues(ttt, tt.want.Result[i], got.Result[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func TestServer_ListExecutions(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + + type args struct { + ctx context.Context + dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse) + req *action.ListExecutionsRequest + } + tests := []struct { + name string + args args + want *action.ListExecutionsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.ListExecutionsRequest{}, + }, + wantErr: true, + }, + { + name: "list request single condition", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + + // Set expected response with used values for SetExecution + response.Result[0].CreationDate = resp.GetSetDate() + response.Result[0].ChangeDate = resp.GetSetDate() + response.Result[0].Condition = cond + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }}, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + { + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + }, + }, + }, + { + name: "list request single target", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + // add target as Filter to the request + request.Filters[0] = &action.ExecutionSearchFilter{ + Filter: &action.ExecutionSearchFilter_TargetFilter{ + TargetFilter: &action.TargetFilter{ + TargetId: target.GetId(), + }, + }, + } + cond := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/UpdateAction", + }, + }, + }, + } + resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()}) + + response.Result[0].CreationDate = resp.GetSetDate() + response.Result[0].ChangeDate = resp.GetSetDate() + response.Result[0].Condition = cond + response.Result[0].Targets = []string{target.GetId()} + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{}}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + { + Condition: &action.Condition{}, + Targets: []string{""}, + }, + }, + }, + }, + { + name: "list multiple conditions", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + + request.Filters[0] = &action.ExecutionSearchFilter{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }}, + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/CreateSession", + }, + }, + }}, + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/SetSession", + }, + }, + }}, + }, + }, + }, + } + + cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()}) + response.Result[2] = &action.Execution{ + CreationDate: resp1.GetSetDate(), + ChangeDate: resp1.GetSetDate(), + Condition: cond1, + Targets: []string{targetResp.GetId()}, + } + + cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] + resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()}) + response.Result[1] = &action.Execution{ + CreationDate: resp2.GetSetDate(), + ChangeDate: resp2.GetSetDate(), + Condition: cond2, + Targets: []string{targetResp.GetId()}, + } + + cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] + resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()}) + response.Result[0] = &action.Execution{ + CreationDate: resp3.GetSetDate(), + ChangeDate: resp3.GetSetDate(), + Condition: cond3, + Targets: []string{targetResp.GetId()}, + } + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{ + {}, + }, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple conditions all types", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + conditions := request.Filters[0].GetInConditionsFilter().GetConditions() + for i, cond := range conditions { + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + response.Result[(len(conditions)-1)-i] = &action.Execution{ + CreationDate: resp.GetSetDate(), + ChangeDate: resp.GetSetDate(), + Condition: cond, + Targets: []string{targetResp.GetId()}, + } + } + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}}, + }, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 10, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list multiple conditions all types, sort id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + conditions := request.Filters[0].GetInConditionsFilter().GetConditions() + for i, cond := range conditions { + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + response.Result[i] = &action.Execution{ + CreationDate: resp.GetSetDate(), + ChangeDate: resp.GetSetDate(), + Condition: cond, + Targets: []string{targetResp.GetId()}, + } + } + }, + req: &action.ListExecutionsRequest{ + SortingColumn: gu.Ptr(action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID), + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + }, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 10, + AppliedLimit: 100, + }, + Result: []*action.Execution{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ActionV2beta.ListExecutions(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Result, len(tt.want.Result)) { + assert.EqualExportedValues(ttt, got.Result, tt.want.Result) + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func containExecution(t *assert.CollectT, executionList []*action.Execution, execution *action.Execution) bool { + for _, exec := range executionList { + if assert.EqualExportedValues(t, execution, exec) { + return true + } + } + return false +} diff --git a/internal/api/grpc/action/v2beta/integration_test/server_test.go b/internal/api/grpc/action/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..07ee051c63 --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/server_test.go @@ -0,0 +1,23 @@ +//go:build integration + +package action_test + +import ( + "context" + "os" + "testing" + "time" +) + +var ( + CTX context.Context +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + return m.Run() + }()) +} diff --git a/internal/api/grpc/action/v2beta/integration_test/target_test.go b/internal/api/grpc/action/v2beta/integration_test/target_test.go new file mode 100644 index 0000000000..8238d3146d --- /dev/null +++ b/internal/api/grpc/action/v2beta/integration_test/target_test.go @@ -0,0 +1,550 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" +) + +func TestServer_CreateTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type want struct { + id bool + creationDate bool + signingKey bool + } + alreadyExistingTargetName := gofakeit.AppName() + instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", domain.TargetTypeAsync, false) + tests := []struct { + name string + ctx context.Context + req *action.CreateTargetRequest + want + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + }, + wantErr: true, + }, + { + name: "empty name", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: "", + }, + wantErr: true, + }, + { + name: "empty type", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: nil, + }, + wantErr: true, + }, + { + name: "empty webhook url", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + }, + wantErr: true, + }, + { + name: "empty request response url", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{}, + }, + }, + wantErr: true, + }, + { + name: "empty timeout", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + Timeout: nil, + }, + wantErr: true, + }, + { + name: "async, already existing, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: alreadyExistingTargetName, + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + wantErr: true, + }, + { + name: "async, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "webhook, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "webhook, interrupt on error, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "call, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + + { + name: "call, interruptOnError, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2beta.CreateTarget(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateTargetResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, tt.want.signingKey, got) + }) + } +} + +func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID, expectedSigningKey bool, actualResp *action.CreateTargetResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } + + if expectedID { + assert.NotEmpty(t, actualResp.GetId()) + } else { + assert.Nil(t, actualResp.Id) + } + + if expectedSigningKey { + assert.NotEmpty(t, actualResp.GetSigningKey()) + } else { + assert.Nil(t, actualResp.SigningKey) + } +} + +func TestServer_UpdateTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + req *action.UpdateTargetRequest + } + type want struct { + change bool + changeDate bool + signingKey bool + } + tests := []struct { + name string + prepare func(request *action.UpdateTargetRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *action.UpdateTargetRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Endpoint: gu.Ptr("https://example.com"), + }, + }, + want: want{ + change: false, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change name, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "regenerate signingkey, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + ExpirationSigningKey: durationpb.New(0 * time.Second), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: true, + }, + }, + { + name: "change type, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + TargetType: &action.UpdateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change url, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Endpoint: gu.Ptr("https://example.com/hooks/new"), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change timeout, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Timeout: durationpb.New(20 * time.Second), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change type async, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + TargetType: &action.UpdateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ActionV2beta.UpdateTarget(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateTargetResponse(t, creationDate, changeDate, tt.want.changeDate, tt.want.signingKey, got) + }) + } +} + +func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate, expectedSigningKey bool, actualResp *action.UpdateTargetResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } + + if expectedSigningKey { + assert.NotEmpty(t, actualResp.GetSigningKey()) + } else { + assert.Nil(t, actualResp.SigningKey) + } +} + +func TestServer_DeleteTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + tests := []struct { + name string + ctx context.Context + prepare func(request *action.DeleteTargetRequest) (time.Time, time.Time) + req *action.DeleteTargetRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + req: &action.DeleteTargetRequest{ + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "empty id", + ctx: iamOwnerCtx, + req: &action.DeleteTargetRequest{ + Id: "", + }, + wantErr: true, + }, + { + name: "delete target, not existing", + ctx: iamOwnerCtx, + req: &action.DeleteTargetRequest{ + Id: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete target", + ctx: iamOwnerCtx, + prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + return creationDate, time.Time{} + }, + req: &action.DeleteTargetRequest{}, + wantDeletionDate: true, + }, + { + name: "delete target, already removed", + ctx: iamOwnerCtx, + prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + instance.DeleteTarget(iamOwnerCtx, t, targetID) + return creationDate, time.Now().UTC() + }, + req: &action.DeleteTargetRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.ActionV2beta.DeleteTarget(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteTargetResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteTargetResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *action.DeleteTargetResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} diff --git a/internal/api/grpc/resources/action/v3alpha/query.go b/internal/api/grpc/action/v2beta/query.go similarity index 77% rename from internal/api/grpc/resources/action/v3alpha/query.go rename to internal/api/grpc/action/v2beta/query.go index 7cdedd8134..66bafa4e7d 100644 --- a/internal/api/grpc/resources/action/v3alpha/query.go +++ b/internal/api/grpc/action/v2beta/query.go @@ -5,14 +5,14 @@ import ( "strings" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) const ( @@ -23,10 +23,6 @@ const ( ) func (s *Server) GetTarget(ctx context.Context, req *action.GetTargetRequest) (*action.GetTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - resp, err := s.query.GetTargetByID(ctx, req.GetId()) if err != nil { return nil, err @@ -45,11 +41,8 @@ type Context interface { GetOwner() InstanceContext } -func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsRequest) (*action.SearchTargetsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - queries, err := s.searchTargetsRequestToModel(req) +func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest) (*action.ListTargetsResponse, error) { + queries, err := s.ListTargetsRequestToModel(req) if err != nil { return nil, err } @@ -57,17 +50,14 @@ func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsReq if err != nil { return nil, err } - return &action.SearchTargetsResponse{ - Result: targetsToPb(resp.Targets), - Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse), + return &action.ListTargetsResponse{ + Result: targetsToPb(resp.Targets), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), }, nil } -func (s *Server) SearchExecutions(ctx context.Context, req *action.SearchExecutionsRequest) (*action.SearchExecutionsResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } - queries, err := s.searchExecutionsRequestToModel(req) +func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsRequest) (*action.ListExecutionsResponse, error) { + queries, err := s.ListExecutionsRequestToModel(req) if err != nil { return nil, err } @@ -75,45 +65,50 @@ func (s *Server) SearchExecutions(ctx context.Context, req *action.SearchExecuti if err != nil { return nil, err } - return &action.SearchExecutionsResponse{ - Result: executionsToPb(resp.Executions), - Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse), + return &action.ListExecutionsResponse{ + Result: executionsToPb(resp.Executions), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), }, nil } -func targetsToPb(targets []*query.Target) []*action.GetTarget { - t := make([]*action.GetTarget, len(targets)) +func targetsToPb(targets []*query.Target) []*action.Target { + t := make([]*action.Target, len(targets)) for i, target := range targets { t[i] = targetToPb(target) } return t } -func targetToPb(t *query.Target) *action.GetTarget { - target := &action.GetTarget{ - Details: resource_object.DomainToDetailsPb(&t.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, t.ResourceOwner), - Config: &action.Target{ - Name: t.Name, - Timeout: durationpb.New(t.Timeout), - Endpoint: t.Endpoint, - }, +func targetToPb(t *query.Target) *action.Target { + target := &action.Target{ + Id: t.ObjectDetails.ID, + Name: t.Name, + Timeout: durationpb.New(t.Timeout), + Endpoint: t.Endpoint, SigningKey: t.SigningKey, } switch t.TargetType { case domain.TargetTypeWebhook: - target.Config.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}} + target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.RESTWebhook{InterruptOnError: t.InterruptOnError}} case domain.TargetTypeCall: - target.Config.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}} + target.TargetType = &action.Target_RestCall{RestCall: &action.RESTCall{InterruptOnError: t.InterruptOnError}} case domain.TargetTypeAsync: - target.Config.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}} + target.TargetType = &action.Target_RestAsync{RestAsync: &action.RESTAsync{}} default: - target.Config.TargetType = nil + target.TargetType = nil + } + + if !t.ObjectDetails.EventDate.IsZero() { + target.ChangeDate = timestamppb.New(t.ObjectDetails.EventDate) + } + if !t.ObjectDetails.CreationDate.IsZero() { + target.CreationDate = timestamppb.New(t.ObjectDetails.CreationDate) } return target } -func (s *Server) searchTargetsRequestToModel(req *action.SearchTargetsRequest) (*query.TargetSearchQueries, error) { - offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query) +func (s *Server) ListTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) if err != nil { return nil, err } @@ -155,7 +150,7 @@ func targetQueryToQuery(filter *action.TargetSearchFilter) (query.SearchQuery, e } func targetNameQueryToQuery(q *action.TargetNameFilter) (query.SearchQuery, error) { - return query.NewTargetNameSearchQuery(resource_object.TextMethodPbToQuery(q.Method), q.GetTargetName()) + return query.NewTargetNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetTargetName()) } func targetInTargetIdsQueryToQuery(q *action.InTargetIDsFilter) (query.SearchQuery, error) { @@ -210,8 +205,8 @@ func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.C } } -func (s *Server) searchExecutionsRequestToModel(req *action.SearchExecutionsRequest) (*query.ExecutionSearchQueries, error) { - offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query) +func (s *Server) ListExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) if err != nil { return nil, err } @@ -247,12 +242,6 @@ func executionQueryToQuery(searchQuery *action.ExecutionSearchFilter) (query.Sea return inConditionsQueryToQuery(q.InConditionsFilter) case *action.ExecutionSearchFilter_ExecutionTypeFilter: return executionTypeToQuery(q.ExecutionTypeFilter) - case *action.ExecutionSearchFilter_IncludeFilter: - include, err := conditionToInclude(q.IncludeFilter.GetInclude()) - if err != nil { - return nil, err - } - return query.NewIncludeSearchQuery(include) case *action.ExecutionSearchFilter_TargetFilter: return query.NewTargetSearchQuery(q.TargetFilter.GetTargetId()) default: @@ -319,35 +308,38 @@ func conditionToID(q *action.Condition) (string, error) { } } -func executionsToPb(executions []*query.Execution) []*action.GetExecution { - e := make([]*action.GetExecution, len(executions)) +func executionsToPb(executions []*query.Execution) []*action.Execution { + e := make([]*action.Execution, len(executions)) for i, execution := range executions { e[i] = executionToPb(execution) } return e } -func executionToPb(e *query.Execution) *action.GetExecution { - targets := make([]*action.ExecutionTargetType, len(e.Targets)) +func executionToPb(e *query.Execution) *action.Execution { + targets := make([]string, len(e.Targets)) for i := range e.Targets { switch e.Targets[i].Type { - case domain.ExecutionTargetTypeInclude: - targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Include{Include: executionIDToCondition(e.Targets[i].Target)}} case domain.ExecutionTargetTypeTarget: - targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Target{Target: e.Targets[i].Target}} - case domain.ExecutionTargetTypeUnspecified: + targets[i] = e.Targets[i].Target + case domain.ExecutionTargetTypeInclude, domain.ExecutionTargetTypeUnspecified: continue default: continue } } - return &action.GetExecution{ - Details: resource_object.DomainToDetailsPb(&e.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, e.ResourceOwner), - Execution: &action.Execution{ - Targets: targets, - }, + exec := &action.Execution{ + Condition: executionIDToCondition(e.ID), + Targets: targets, } + if !e.ObjectDetails.EventDate.IsZero() { + exec.ChangeDate = timestamppb.New(e.ObjectDetails.EventDate) + } + if !e.ObjectDetails.CreationDate.IsZero() { + exec.CreationDate = timestamppb.New(e.ObjectDetails.CreationDate) + } + return exec } func executionIDToCondition(include string) *action.Condition { diff --git a/internal/api/grpc/resources/action/v3alpha/server.go b/internal/api/grpc/action/v2beta/server.go similarity index 66% rename from internal/api/grpc/resources/action/v3alpha/server.go rename to internal/api/grpc/action/v2beta/server.go index b80c60d668..ef0d8eb2ba 100644 --- a/internal/api/grpc/resources/action/v3alpha/server.go +++ b/internal/api/grpc/action/v2beta/server.go @@ -1,8 +1,6 @@ package action import ( - "context" - "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" @@ -10,14 +8,13 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) -var _ action.ZITADELActionsServer = (*Server)(nil) +var _ action.ActionServiceServer = (*Server)(nil) type Server struct { - action.UnimplementedZITADELActionsServer + action.UnimplementedActionServiceServer systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries @@ -47,28 +44,21 @@ func CreateServer( } func (s *Server) RegisterServer(grpcServer *grpc.Server) { - action.RegisterZITADELActionsServer(grpcServer, s) + action.RegisterActionServiceServer(grpcServer, s) } func (s *Server) AppName() string { - return action.ZITADELActions_ServiceDesc.ServiceName + return action.ActionService_ServiceDesc.ServiceName } func (s *Server) MethodPrefix() string { - return action.ZITADELActions_ServiceDesc.ServiceName + return action.ActionService_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { - return action.ZITADELActions_AuthMethods + return action.ActionService_AuthMethods } func (s *Server) RegisterGateway() server.RegisterGatewayFunc { - return action.RegisterZITADELActionsHandler -} - -func checkActionsEnabled(ctx context.Context) error { - if authz.GetInstance(ctx).Features().Actions { - return nil - } - return zerrors.ThrowPreconditionFailed(nil, "ACTION-8o6pvqfjhs", "Errors.Action.NotEnabled") + return action.RegisterActionServiceHandler } diff --git a/internal/api/grpc/resources/action/v3alpha/target.go b/internal/api/grpc/action/v2beta/target.go similarity index 50% rename from internal/api/grpc/resources/action/v3alpha/target.go rename to internal/api/grpc/action/v2beta/target.go index 621b6677b7..26c88b9683 100644 --- a/internal/api/grpc/resources/action/v3alpha/target.go +++ b/internal/api/grpc/action/v2beta/target.go @@ -4,121 +4,122 @@ import ( "context" "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/authz" - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } add := createTargetToCommand(req) instanceID := authz.GetInstance(ctx).InstanceID() - details, err := s.command.AddTarget(ctx, add, instanceID) + createdAt, err := s.command.AddTarget(ctx, add, instanceID) if err != nil { return nil, err } + var creationDate *timestamppb.Timestamp + if !createdAt.IsZero() { + creationDate = timestamppb.New(createdAt) + } return &action.CreateTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), - SigningKey: add.SigningKey, + Id: add.AggregateID, + CreationDate: creationDate, + SigningKey: add.SigningKey, }, nil } -func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } +func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetRequest) (*action.UpdateTargetResponse, error) { instanceID := authz.GetInstance(ctx).InstanceID() - patch := patchTargetToCommand(req) - details, err := s.command.ChangeTarget(ctx, patch, instanceID) + update := updateTargetToCommand(req) + changedAt, err := s.command.ChangeTarget(ctx, update, instanceID) if err != nil { return nil, err } - return &action.PatchTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), - SigningKey: patch.SigningKey, + var changeDate *timestamppb.Timestamp + if !changedAt.IsZero() { + changeDate = timestamppb.New(changedAt) + } + return &action.UpdateTargetResponse{ + ChangeDate: changeDate, + SigningKey: update.SigningKey, }, nil } func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) { - if err := checkActionsEnabled(ctx); err != nil { - return nil, err - } instanceID := authz.GetInstance(ctx).InstanceID() - details, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID) + deletedAt, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID) if err != nil { return nil, err } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } return &action.DeleteTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + DeletionDate: deletionDate, }, nil } func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget { - reqTarget := req.GetTarget() var ( targetType domain.TargetType interruptOnError bool ) - switch t := reqTarget.GetTargetType().(type) { - case *action.Target_RestWebhook: + switch t := req.GetTargetType().(type) { + case *action.CreateTargetRequest_RestWebhook: targetType = domain.TargetTypeWebhook interruptOnError = t.RestWebhook.InterruptOnError - case *action.Target_RestCall: + case *action.CreateTargetRequest_RestCall: targetType = domain.TargetTypeCall interruptOnError = t.RestCall.InterruptOnError - case *action.Target_RestAsync: + case *action.CreateTargetRequest_RestAsync: targetType = domain.TargetTypeAsync } return &command.AddTarget{ - Name: reqTarget.GetName(), + Name: req.GetName(), TargetType: targetType, - Endpoint: reqTarget.GetEndpoint(), - Timeout: reqTarget.GetTimeout().AsDuration(), + Endpoint: req.GetEndpoint(), + Timeout: req.GetTimeout().AsDuration(), InterruptOnError: interruptOnError, } } -func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget { +func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget { expirationSigningKey := false // TODO handle expiration, currently only immediate expiration is supported - if req.GetTarget().GetExpirationSigningKey() != nil { + if req.GetExpirationSigningKey() != nil { expirationSigningKey = true } - reqTarget := req.GetTarget() - if reqTarget == nil { + if req == nil { return nil } target := &command.ChangeTarget{ ObjectRoot: models.ObjectRoot{ AggregateID: req.GetId(), }, - Name: reqTarget.Name, - Endpoint: reqTarget.Endpoint, + Name: req.Name, + Endpoint: req.Endpoint, ExpirationSigningKey: expirationSigningKey, } - if reqTarget.TargetType != nil { - switch t := reqTarget.GetTargetType().(type) { - case *action.PatchTarget_RestWebhook: + if req.TargetType != nil { + switch t := req.GetTargetType().(type) { + case *action.UpdateTargetRequest_RestWebhook: target.TargetType = gu.Ptr(domain.TargetTypeWebhook) target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError) - case *action.PatchTarget_RestCall: + case *action.UpdateTargetRequest_RestCall: target.TargetType = gu.Ptr(domain.TargetTypeCall) target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError) - case *action.PatchTarget_RestAsync: + case *action.UpdateTargetRequest_RestAsync: target.TargetType = gu.Ptr(domain.TargetTypeAsync) target.InterruptOnError = gu.Ptr(false) } } - if reqTarget.Timeout != nil { - target.Timeout = gu.Ptr(reqTarget.GetTimeout().AsDuration()) + if req.Timeout != nil { + target.Timeout = gu.Ptr(req.GetTimeout().AsDuration()) } return target } diff --git a/internal/api/grpc/resources/action/v3alpha/target_test.go b/internal/api/grpc/action/v2beta/target_test.go similarity index 79% rename from internal/api/grpc/resources/action/v3alpha/target_test.go rename to internal/api/grpc/action/v2beta/target_test.go index f4e0d02e3b..b18ee52160 100644 --- a/internal/api/grpc/resources/action/v3alpha/target_test.go +++ b/internal/api/grpc/action/v2beta/target_test.go @@ -10,12 +10,12 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) func Test_createTargetToCommand(t *testing.T) { type args struct { - req *action.Target + req *action.CreateTargetRequest } tests := []struct { name string @@ -34,11 +34,11 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (webhook)", - args: args{&action.Target{ + args: args{&action.CreateTargetRequest{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, }, Timeout: durationpb.New(10 * time.Second), }}, @@ -52,11 +52,11 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (async)", - args: args{&action.Target{ + args: args{&action.CreateTargetRequest{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, }, Timeout: durationpb.New(10 * time.Second), }}, @@ -70,11 +70,11 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (interrupting response)", - args: args{&action.Target{ + args: args{&action.CreateTargetRequest{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ InterruptOnError: true, }, }, @@ -91,7 +91,7 @@ func Test_createTargetToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := createTargetToCommand(&action.CreateTargetRequest{Target: tt.args.req}) + got := createTargetToCommand(tt.args.req) assert.Equal(t, tt.want, got) }) } @@ -99,7 +99,7 @@ func Test_createTargetToCommand(t *testing.T) { func Test_updateTargetToCommand(t *testing.T) { type args struct { - req *action.PatchTarget + req *action.UpdateTargetRequest } tests := []struct { name string @@ -113,7 +113,7 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields nil", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: nil, TargetType: nil, Timeout: nil, @@ -128,7 +128,7 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields empty", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr(""), TargetType: nil, Timeout: durationpb.New(0), @@ -143,11 +143,11 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (webhook)", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.PatchTarget_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ + TargetType: &action.UpdateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ InterruptOnError: false, }, }, @@ -163,11 +163,11 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (webhook interrupt)", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.PatchTarget_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ + TargetType: &action.UpdateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ InterruptOnError: true, }, }, @@ -183,11 +183,11 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (async)", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.PatchTarget_RestAsync{ - RestAsync: &action.SetRESTAsync{}, + TargetType: &action.UpdateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, }, Timeout: durationpb.New(10 * time.Second), }}, @@ -201,11 +201,11 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (interrupting response)", - args: args{&action.PatchTarget{ + args: args{&action.UpdateTargetRequest{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.PatchTarget_RestCall{ - RestCall: &action.SetRESTCall{ + TargetType: &action.UpdateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ InterruptOnError: true, }, }, @@ -222,7 +222,7 @@ func Test_updateTargetToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := patchTargetToCommand(&action.PatchTargetRequest{Target: tt.args.req}) + got := updateTargetToCommand(tt.args.req) assert.Equal(t, tt.want, got) }) } diff --git a/internal/api/grpc/admin/iam_member.go b/internal/api/grpc/admin/iam_member.go index edd4dd0ce6..8f9b11ce2a 100644 --- a/internal/api/grpc/admin/iam_member.go +++ b/internal/api/grpc/admin/iam_member.go @@ -28,8 +28,7 @@ func (s *Server) ListIAMMembers(ctx context.Context, req *admin_pb.ListIAMMember } return &admin_pb.ListIAMMembersResponse{ Details: object.ToListDetails(res.Count, res.Sequence, res.LastRun), - //TODO: resource owner of user of the member instead of the membership resource owner - Result: member.MembersToPb("", res.Members), + Result: member.MembersToPb("", res.Members), }, nil } diff --git a/internal/api/grpc/admin/iam_member_converter.go b/internal/api/grpc/admin/iam_member_converter.go index 07e91d21b3..2fe75214fd 100644 --- a/internal/api/grpc/admin/iam_member_converter.go +++ b/internal/api/grpc/admin/iam_member_converter.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + member_pb "github.com/zitadel/zitadel/pkg/grpc/member" ) func AddIAMMemberToDomain(req *admin_pb.AddIAMMemberRequest) *domain.Member { @@ -31,12 +32,29 @@ func ListIAMMembersRequestToQuery(req *admin_pb.ListIAMMembersRequest) (*query.I return &query.IAMMembersQuery{ MembersQuery: query.MembersQuery{ SearchRequest: query.SearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - // SortingColumn: model.IAMMemberSearchKey, //TOOD: not implemented in proto + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToMemberColumn(req.SortingColumn), }, Queries: queries, }, }, nil } + +func fieldNameToMemberColumn(fieldName member_pb.MemberFieldColumnName) query.Column { + switch fieldName { + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_UNSPECIFIED: + return query.InstanceMemberInstanceID + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_ID: + return query.InstanceMemberUserID + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CREATION_DATE: + return query.InstanceMemberCreationDate + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CHANGE_DATE: + return query.InstanceMemberChangeDate + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_RESOURCE_OWNER: + return query.InstanceMemberResourceOwner + default: + return query.Column{} + } +} diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 67e40a44ab..3084031a73 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -215,6 +215,7 @@ func addGenericOAuthProviderToCommand(req *admin_pb.AddGenericOAuthProviderReque UserEndpoint: req.UserEndpoint, Scopes: req.Scopes, IDAttribute: req.IdAttribute, + UsePKCE: req.UsePkce, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } @@ -229,6 +230,7 @@ func updateGenericOAuthProviderToCommand(req *admin_pb.UpdateGenericOAuthProvide UserEndpoint: req.UserEndpoint, Scopes: req.Scopes, IDAttribute: req.IdAttribute, + UsePKCE: req.UsePkce, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } @@ -241,6 +243,7 @@ func addGenericOIDCProviderToCommand(req *admin_pb.AddGenericOIDCProviderRequest ClientSecret: req.ClientSecret, Scopes: req.Scopes, IsIDTokenMapping: req.IsIdTokenMapping, + UsePKCE: req.UsePkce, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } @@ -253,6 +256,7 @@ func updateGenericOIDCProviderToCommand(req *admin_pb.UpdateGenericOIDCProviderR ClientSecret: req.ClientSecret, Scopes: req.Scopes, IsIDTokenMapping: req.IsIdTokenMapping, + UsePKCE: req.UsePkce, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } @@ -423,6 +427,7 @@ func addLDAPProviderToCommand(req *admin_pb.AddLDAPProviderRequest) command.LDAP UserObjectClasses: req.UserObjectClasses, UserFilters: req.UserFilters, Timeout: req.Timeout.AsDuration(), + RootCA: req.RootCa, LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } @@ -442,6 +447,7 @@ func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) comman Timeout: req.Timeout.AsDuration(), LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + RootCA: req.RootCa, } } diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 7f7443fef7..5bbcab27cf 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -4,7 +4,8 @@ import ( "context" "encoding/base64" "fmt" - "io/ioutil" + "io" + "os" "strconv" "time" @@ -214,7 +215,7 @@ func (s *Server) transportDataFromFile(ctx context.Context, v1Transformation boo data = s3Data } if localInput != nil { - localData, err := ioutil.ReadFile(localInput.Path) + localData, err := os.ReadFile(localInput.Path) if err != nil { return nil, err } @@ -274,7 +275,7 @@ func getFileFromS3(ctx context.Context, input *admin_pb.ImportDataRequest_S3Inpu } defer object.Close() - return ioutil.ReadAll(object) + return io.ReadAll(object) } func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSInput) (_ []byte, err error) { @@ -297,7 +298,7 @@ func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSIn return nil, err } defer reader.Close() - return ioutil.ReadAll(reader) + return io.ReadAll(reader) } func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, ctxData authz.CtxData, org *admin_pb.DataOrg, success *admin_pb.ImportDataSuccess, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) (err error) { diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 934de1b570..293e7c74d7 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -112,9 +112,6 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain str if err != nil { return nil, err } - if err != nil { - return nil, err - } userIDs := make([]string, len(users.Users)) for i, user := range users.Users { userIDs[i] = user.ID diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 90e0ddc1d6..13f955fd81 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -69,7 +69,6 @@ func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserC } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). OrderDesc(). AwaitOpenTransactions(). diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index fee4450ce2..baa45c6c6e 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -22,7 +22,6 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, - Actions: req.Actions, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, @@ -41,7 +40,6 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent), @@ -62,7 +60,6 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, TokenExchange: req.OidcTokenExchange, - Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, @@ -71,6 +68,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com EnableBackChannelLogout: req.EnableBackChannelLogout, LoginV2: loginV2, PermissionCheckV2: req.PermissionCheckV2, + ConsoleUseV2UserApi: req.ConsoleUseV2UserApi, }, nil } @@ -82,7 +80,6 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), @@ -91,6 +88,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout), LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2), PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2), + ConsoleUseV2UserApi: featureSourceToFlagPb(&f.ConsoleUseV2UserApi), } } diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index bf87dc959b..f481e4f65a 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -23,7 +23,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { OidcTriggerIntrospectionProjections: gu.Ptr(false), OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OidcSingleV1SessionTermination: gu.Ptr(true), @@ -37,7 +36,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -74,10 +72,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -132,10 +126,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_SYSTEM, - }, ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{ ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -173,7 +163,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), DebugOidcParentError: gu.Ptr(true), @@ -183,6 +172,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) { Required: true, BaseUri: gu.Ptr("https://login.com"), }, + ConsoleUseV2UserApi: gu.Ptr(true), } want := &command.InstanceFeatures{ LoginDefaultOrg: gu.Ptr(true), @@ -190,7 +180,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { LegacyIntrospection: nil, UserSchema: gu.Ptr(true), TokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), DebugOIDCParentError: gu.Ptr(true), @@ -200,6 +189,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) { Required: true, BaseURI: &url.URL{Scheme: "https", Host: "login.com"}, }, + ConsoleUseV2UserApi: gu.Ptr(true), } got, err := instanceFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -229,10 +219,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -264,6 +250,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, + ConsoleUseV2UserApi: query.FeatureSource[bool]{ + Level: feature.LevelInstance, + Value: true, + }, } want := &feature_pb.GetInstanceFeaturesResponse{ Details: &object.Details{ @@ -287,10 +277,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, OidcTokenExchange: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -328,6 +314,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, + ConsoleUseV2UserApi: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_INSTANCE, + }, } got := instanceFeaturesToPb(arg) assert.Equal(t, want, got) diff --git a/internal/api/grpc/feature/v2/integration_test/feature_test.go b/internal/api/grpc/feature/v2/integration_test/feature_test.go index 2af4f642c4..f27b57ff8c 100644 --- a/internal/api/grpc/feature/v2/integration_test/feature_test.go +++ b/internal/api/grpc/feature/v2/integration_test/feature_test.go @@ -158,14 +158,6 @@ func TestServer_GetSystemFeatures(t *testing.T) { want *feature.GetSystemFeaturesResponse wantErr bool }{ - { - name: "permission error", - args: args{ - ctx: IamCTX, - req: &feature.GetSystemFeaturesRequest{}, - }, - wantErr: true, - }, { name: "nothing set", args: args{ @@ -219,7 +211,6 @@ func TestServer_GetSystemFeatures(t *testing.T) { assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections) assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection) assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema) - assertFeatureFlag(t, tt.want.Actions, got.Actions) }) } } @@ -349,14 +340,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { want *feature.GetInstanceFeaturesResponse wantErr bool }{ - { - name: "permission error", - args: args{ - ctx: OrgCTX, - req: &feature.GetInstanceFeaturesRequest{}, - }, - wantErr: true, - }, { name: "defaults, no inheritance", args: args{ @@ -390,10 +373,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, { @@ -403,7 +382,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { LoginDefaultOrg: gu.Ptr(true), OidcTriggerIntrospectionProjections: gu.Ptr(false), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), }) require.NoError(t, err) }, @@ -424,10 +402,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - Actions: &feature.FeatureFlag{ - Enabled: true, - Source: feature.Source_SOURCE_INSTANCE, - }, }, }, { @@ -461,10 +435,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, } diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index 39f2284beb..bbb375716e 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -14,7 +14,6 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command. TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, - Actions: req.Actions, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, @@ -29,7 +28,6 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } @@ -42,7 +40,6 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm LegacyIntrospection: req.OidcLegacyIntrospection, UserSchema: req.UserSchema, TokenExchange: req.OidcTokenExchange, - Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, @@ -58,7 +55,6 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), diff --git a/internal/api/grpc/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go index 2896d8f77b..72d91b10d4 100644 --- a/internal/api/grpc/feature/v2beta/converter_test.go +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -22,7 +22,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { OidcTriggerIntrospectionProjections: gu.Ptr(false), OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OidcSingleV1SessionTermination: gu.Ptr(true), @@ -32,7 +31,6 @@ func Test_systemFeaturesToCommand(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: nil, UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -64,10 +62,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -107,10 +101,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_SYSTEM, - }, ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{ ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -131,7 +121,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { OidcLegacyIntrospection: nil, UserSchema: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), OidcSingleV1SessionTermination: gu.Ptr(true), @@ -142,7 +131,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { LegacyIntrospection: nil, UserSchema: gu.Ptr(true), TokenExchange: gu.Ptr(true), - Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), @@ -174,10 +162,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, - Actions: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, TokenExchange: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -217,10 +201,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, - Actions: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, OidcTokenExchange: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_SYSTEM, diff --git a/internal/api/grpc/feature/v2beta/integration_test/feature_test.go b/internal/api/grpc/feature/v2beta/integration_test/feature_test.go index 69e05352d0..cbd9f5f939 100644 --- a/internal/api/grpc/feature/v2beta/integration_test/feature_test.go +++ b/internal/api/grpc/feature/v2beta/integration_test/feature_test.go @@ -202,10 +202,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, { @@ -215,7 +211,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { LoginDefaultOrg: gu.Ptr(true), OidcTriggerIntrospectionProjections: gu.Ptr(false), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), }) require.NoError(t, err) }, @@ -236,10 +231,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - Actions: &feature.FeatureFlag{ - Enabled: true, - Source: feature.Source_SOURCE_INSTANCE, - }, }, }, { @@ -273,10 +264,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - Actions: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, }, }, } diff --git a/internal/api/grpc/filter/v2beta/converter.go b/internal/api/grpc/filter/v2beta/converter.go new file mode 100644 index 0000000000..e34f9dd9d7 --- /dev/null +++ b/internal/api/grpc/filter/v2beta/converter.go @@ -0,0 +1,56 @@ +package filter + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" +) + +func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison { + switch method { + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS: + return query.TextEquals + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE: + return query.TextEqualsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH: + return query.TextStartsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE: + return query.TextStartsWithIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS: + return query.TextContains + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE: + return query.TextContainsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH: + return query.TextEndsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE: + return query.TextEndsWithIgnoreCase + default: + return -1 + } +} + +func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) { + limit = defaults.DefaultQueryLimit + if query == nil { + return 0, limit, asc, nil + } + offset = query.Offset + asc = query.Asc + if defaults.MaxQueryLimit > 0 && uint64(query.Limit) > defaults.MaxQueryLimit { + return 0, 0, false, zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", query.Limit, defaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded") + } + if query.Limit > 0 { + limit = uint64(query.Limit) + } + return offset, limit, asc, nil +} + +func QueryToPaginationPb(request query.SearchRequest, response query.SearchResponse) *filter.PaginationResponse { + return &filter.PaginationResponse{ + AppliedLimit: request.Limit, + TotalResult: response.Count, + } +} diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index 9b3e09a10c..6269f122c0 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -504,6 +504,7 @@ func oauthConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.OAut UserEndpoint: template.UserEndpoint, Scopes: template.Scopes, IdAttribute: template.IDAttribute, + UsePkce: template.UsePKCE, }, } } @@ -515,6 +516,7 @@ func oidcConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.OIDCI Issuer: template.Issuer, Scopes: template.Scopes, IsIdTokenMapping: template.IsIDTokenMapping, + UsePkce: template.UsePKCE, }, } } @@ -620,6 +622,7 @@ func ldapConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.LDAPI UserObjectClasses: template.UserObjectClasses, UserFilters: template.UserFilters, Timeout: timeout, + RootCa: template.RootCA, Attributes: ldapAttributesToPb(template.LDAPAttributes), }, } diff --git a/internal/api/grpc/idp/v2/integration_test/query_test.go b/internal/api/grpc/idp/v2/integration_test/query_test.go index 7bfa286b5e..2c4be8a73b 100644 --- a/internal/api/grpc/idp/v2/integration_test/query_test.go +++ b/internal/api/grpc/idp/v2/integration_test/query_test.go @@ -76,6 +76,7 @@ func TestServer_GetIDPByID(t *testing.T) { name, &object.Details{ Sequence: resp.Details.Sequence, + CreationDate: resp.Details.CreationDate, ChangeDate: resp.Details.ChangeDate, ResourceOwner: resp.Details.ResourceOwner, }} @@ -124,6 +125,7 @@ func TestServer_GetIDPByID(t *testing.T) { name, &object.Details{ Sequence: resp.Details.Sequence, + CreationDate: resp.Details.CreationDate, ChangeDate: resp.Details.ChangeDate, ResourceOwner: resp.Details.ResourceOwner, }} @@ -145,6 +147,7 @@ func TestServer_GetIDPByID(t *testing.T) { name, &object.Details{ Sequence: resp.Details.Sequence, + CreationDate: resp.Details.CreationDate, ChangeDate: resp.Details.ChangeDate, ResourceOwner: resp.Details.ResourceOwner, }} @@ -193,6 +196,7 @@ func TestServer_GetIDPByID(t *testing.T) { name, &object.Details{ Sequence: resp.Details.Sequence, + CreationDate: resp.Details.CreationDate, ChangeDate: resp.Details.ChangeDate, ResourceOwner: resp.Details.ResourceOwner, }} diff --git a/internal/api/grpc/idp/v2/query.go b/internal/api/grpc/idp/v2/query.go index 0b66175c14..de78775804 100644 --- a/internal/api/grpc/idp/v2/query.go +++ b/internal/api/grpc/idp/v2/query.go @@ -31,6 +31,7 @@ func idpToPb(idp *query.IDPTemplate) *idp_pb.IDP { Sequence: idp.Sequence, EventDate: idp.ChangeDate, ResourceOwner: idp.ResourceOwner, + CreationDate: idp.CreationDate, }), State: idpStateToPb(idp.State), Name: idp.Name, @@ -288,6 +289,7 @@ func ldapConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.LDAPIDPTemplate UserObjectClasses: template.UserObjectClasses, UserFilters: template.UserFilters, Timeout: timeout, + RootCa: template.RootCA, Attributes: ldapAttributesToPb(template.LDAPAttributes), }, } diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index ef3914cc96..4f68c5f919 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -209,6 +209,7 @@ func addGenericOAuthProviderToCommand(req *mgmt_pb.AddGenericOAuthProviderReques Scopes: req.Scopes, IDAttribute: req.IdAttribute, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + UsePKCE: req.UsePkce, } } @@ -223,6 +224,7 @@ func updateGenericOAuthProviderToCommand(req *mgmt_pb.UpdateGenericOAuthProvider Scopes: req.Scopes, IDAttribute: req.IdAttribute, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + UsePKCE: req.UsePkce, } } @@ -234,6 +236,7 @@ func addGenericOIDCProviderToCommand(req *mgmt_pb.AddGenericOIDCProviderRequest) ClientSecret: req.ClientSecret, Scopes: req.Scopes, IsIDTokenMapping: req.IsIdTokenMapping, + UsePKCE: req.UsePkce, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } @@ -246,6 +249,7 @@ func updateGenericOIDCProviderToCommand(req *mgmt_pb.UpdateGenericOIDCProviderRe ClientSecret: req.ClientSecret, Scopes: req.Scopes, IsIDTokenMapping: req.IsIdTokenMapping, + UsePKCE: req.UsePkce, IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } @@ -416,6 +420,7 @@ func addLDAPProviderToCommand(req *mgmt_pb.AddLDAPProviderRequest) command.LDAPP UserObjectClasses: req.UserObjectClasses, UserFilters: req.UserFilters, Timeout: req.Timeout.AsDuration(), + RootCA: req.RootCa, LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } @@ -435,6 +440,7 @@ func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command Timeout: req.Timeout.AsDuration(), LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + RootCA: req.RootCa, } } diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index d25d46d852..70f509a4d7 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -50,7 +50,6 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). OrderDesc(). AwaitOpenTransactions(). diff --git a/internal/api/grpc/management/project.go b/internal/api/grpc/management/project.go index 00ccbd215c..52b6b10e9a 100644 --- a/internal/api/grpc/management/project.go +++ b/internal/api/grpc/management/project.go @@ -70,7 +70,6 @@ func (s *Server) ListProjectGrantChanges(ctx context.Context, req *mgmt_pb.ListP } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). OrderDesc(). ResourceOwner(authz.GetCtxData(ctx).OrgID). @@ -152,7 +151,6 @@ func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjec } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). AwaitOpenTransactions(). OrderDesc(). diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index 15e057c1bd..3a0e1d5f92 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -52,7 +52,6 @@ func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChanges } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). AwaitOpenTransactions(). OrderDesc(). @@ -98,7 +97,11 @@ func (s *Server) AddOIDCApp(ctx context.Context, req *mgmt_pb.AddOIDCAppRequest) }, nil } func (s *Server) AddSAMLApp(ctx context.Context, req *mgmt_pb.AddSAMLAppRequest) (*mgmt_pb.AddSAMLAppResponse, error) { - app, err := s.command.AddSAMLApplication(ctx, AddSAMLAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + samlApp, err := AddSAMLAppRequestToDomain(req) + if err != nil { + return nil, err + } + app, err := s.command.AddSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -150,7 +153,11 @@ func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOID } func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAMLAppConfigRequest) (*mgmt_pb.UpdateSAMLAppConfigResponse, error) { - config, err := s.command.ChangeSAMLApplication(ctx, UpdateSAMLAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + samlApp, err := UpdateSAMLAppConfigRequestToDomain(req) + if err != nil { + return nil, err + } + config, err := s.command.ChangeSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project_application_converter.go b/internal/api/grpc/management/project_application_converter.go index 787470d9c1..13a0048a5b 100644 --- a/internal/api/grpc/management/project_application_converter.go +++ b/internal/api/grpc/management/project_application_converter.go @@ -67,15 +67,21 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp, }, nil } -func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) *domain.SAMLApp { +func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) (*domain.SAMLApp, error) { + loginVersion, loginBaseURI, err := app_grpc.LoginVersionToDomain(req.GetLoginVersion()) + if err != nil { + return nil, err + } return &domain.SAMLApp{ ObjectRoot: models.ObjectRoot{ AggregateID: req.ProjectId, }, - AppName: req.Name, - Metadata: req.GetMetadataXml(), - MetadataURL: req.GetMetadataUrl(), - } + AppName: req.Name, + Metadata: req.GetMetadataXml(), + MetadataURL: req.GetMetadataUrl(), + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil } func AddAPIAppRequestToDomain(app *mgmt_pb.AddAPIAppRequest) *domain.APIApp { @@ -125,15 +131,21 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest) }, nil } -func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) *domain.SAMLApp { +func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) (*domain.SAMLApp, error) { + loginVersion, loginBaseURI, err := app_grpc.LoginVersionToDomain(app.GetLoginVersion()) + if err != nil { + return nil, err + } return &domain.SAMLApp{ ObjectRoot: models.ObjectRoot{ AggregateID: app.ProjectId, }, - AppID: app.AppId, - Metadata: app.GetMetadataXml(), - MetadataURL: app.GetMetadataUrl(), - } + AppID: app.AppId, + Metadata: app.GetMetadataXml(), + MetadataURL: app.GetMetadataUrl(), + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil } func UpdateAPIAppConfigRequestToDomain(app *mgmt_pb.UpdateAPIAppConfigRequest) *domain.APIApp { diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index dac651af81..5b82eb5afe 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -64,7 +64,8 @@ func (s *Server) ListUsers(ctx context.Context, req *mgmt_pb.ListUsersRequest) ( return nil, err } - err = queries.AppendMyResourceOwnerQuery(authz.GetCtxData(ctx).OrgID) + orgID := authz.GetCtxData(ctx).OrgID + err = queries.AppendMyResourceOwnerQuery(orgID) if err != nil { return nil, err } @@ -91,7 +92,6 @@ func (s *Server) ListUserChanges(ctx context.Context, req *mgmt_pb.ListUserChang } query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AllowTimeTravel(). Limit(limit). AwaitOpenTransactions(). OrderDesc(). diff --git a/internal/api/grpc/object/v2/converter.go b/internal/api/grpc/object/v2/converter.go index 8cf0d8b1fa..753b9db095 100644 --- a/internal/api/grpc/object/v2/converter.go +++ b/internal/api/grpc/object/v2/converter.go @@ -20,6 +20,9 @@ func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { if !objectDetail.EventDate.IsZero() { details.ChangeDate = timestamppb.New(objectDetail.EventDate) } + if !objectDetail.CreationDate.IsZero() { + details.CreationDate = timestamppb.New(objectDetail.CreationDate) + } return details } diff --git a/internal/api/grpc/object/v2beta/converter.go b/internal/api/grpc/object/v2beta/converter.go index cc7e02c7fe..9b14bb677a 100644 --- a/internal/api/grpc/object/v2beta/converter.go +++ b/internal/api/grpc/object/v2beta/converter.go @@ -19,6 +19,9 @@ func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { if !objectDetail.EventDate.IsZero() { details.ChangeDate = timestamppb.New(objectDetail.EventDate) } + if !objectDetail.CreationDate.IsZero() { + details.CreationDate = timestamppb.New(objectDetail.CreationDate) + } return details } diff --git a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go index d6b5c7b8cf..64334bd8b1 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -13,6 +13,7 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" @@ -76,19 +77,22 @@ func TestServer_GetAuthRequest(t *testing.T) { now, authRequestID, err := tt.dep() require.NoError(t, err) - got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ - AuthRequestId: authRequestID, - }) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - authRequest := got.GetAuthRequest() - assert.NotNil(t, authRequest) - assert.Equal(t, authRequestID, authRequest.GetId()) - assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) - assert.Contains(t, authRequest.GetScope(), "openid") + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ + AuthRequestId: authRequestID, + }) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + authRequest := got.GetAuthRequest() + assert.NotNil(ttt, authRequest) + assert.Equal(ttt, authRequestID, authRequest.GetId()) + assert.WithinRange(ttt, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) + assert.Contains(ttt, authRequest.GetScope(), "openid") + }, retryDuration, tick) }) } } @@ -103,13 +107,11 @@ func TestServer_CreateCallback(t *testing.T) { sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID) tests := []struct { - name string - ctx context.Context - req *oidc_pb.CreateCallbackRequest - AuthError string - want *oidc_pb.CreateCallbackResponse - wantURL *url.URL - wantErr bool + name string + ctx context.Context + req *oidc_pb.CreateCallbackRequest + want *oidc_pb.CreateCallbackResponse + wantErr bool }{ { name: "Not found", @@ -636,6 +638,233 @@ func TestServer_CreateCallback_Permission(t *testing.T) { } } +func TestServer_GetDeviceAuthorizationRequest(t *testing.T) { + project, err := Instance.CreateProject(CTX) + require.NoError(t, err) + client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) + require.NoError(t, err) + + tests := []struct { + name string + dep func() (*oidc.DeviceAuthorizationResponse, error) + ctx context.Context + want *oidc.DeviceAuthorizationResponse + wantErr bool + }{ + { + name: "Not found", + dep: func() (*oidc.DeviceAuthorizationResponse, error) { + return &oidc.DeviceAuthorizationResponse{ + UserCode: "notFound", + }, nil + }, + ctx: CTX, + wantErr: true, + }, + { + name: "success", + dep: func() (*oidc.DeviceAuthorizationResponse, error) { + return Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid") + }, + ctx: CTX, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deviceAuth, err := tt.dep() + require.NoError(t, err) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetDeviceAuthorizationRequest(tt.ctx, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: deviceAuth.UserCode, + }) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + authRequest := got.GetDeviceAuthorizationRequest() + assert.NotNil(ttt, authRequest) + assert.NotEmpty(ttt, authRequest.GetId()) + assert.Equal(ttt, client.GetClientId(), authRequest.GetClientId()) + assert.Contains(ttt, authRequest.GetScope(), "openid") + assert.NotEmpty(ttt, authRequest.GetAppName()) + assert.NotEmpty(ttt, authRequest.GetProjectName()) + }, retryDuration, tick) + }) + } +} + +func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { + project, err := Instance.CreateProject(CTX) + require.NoError(t, err) + client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) + require.NoError(t, err) + sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID) + + tests := []struct { + name string + ctx context.Context + req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest + AuthError string + want *oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse + wantURL *url.URL + wantErr bool + }{ + { + name: "Not found", + ctx: CTX, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: "123", + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: true, + }, + { + name: "session not found", + ctx: CTX, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid") + require.NoError(t, err) + var id string + assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(t, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, 5*time.Second, 100*time.Millisecond) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: "foo", + SessionToken: "bar", + }, + }, + }, + wantErr: true, + }, + { + name: "session token invalid", + ctx: CTX, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid") + require.NoError(t, err) + var id string + assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(collectT, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, 5*time.Second, 100*time.Millisecond) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: "bar", + }, + }, + }, + wantErr: true, + }, + { + name: "deny device authorization", + ctx: CTX, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid") + require.NoError(t, err) + var id string + assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(collectT, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, 5*time.Second, 100*time.Millisecond) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{}, + }, + want: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{}, + wantErr: false, + }, + { + name: "authorize, no permission, error", + ctx: CTX, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid") + require.NoError(t, err) + var id string + assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(collectT, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, 5*time.Second, 100*time.Millisecond) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: true, + }, + { + name: "authorize, with permission", + ctx: CTXLoginClient, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid") + require.NoError(t, err) + var id string + assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(collectT, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, 5*time.Second, 100*time.Millisecond) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Client.AuthorizeOrDenyDeviceAuthorization(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + func createSession(t *testing.T, ctx context.Context, userID string) *session.CreateSessionResponse { sessionResp, err := Instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ Checks: &session.Checks{ diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index d1ddc35cc0..8612d11558 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -2,6 +2,7 @@ package oidc import ( "context" + "encoding/base64" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/op" @@ -9,7 +10,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/api/http" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -28,6 +29,54 @@ func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequest }, nil } +func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) { + switch v := req.GetCallbackKind().(type) { + case *oidc_pb.CreateCallbackRequest_Error: + return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error) + case *oidc_pb.CreateCallbackRequest_Session: + return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session) + default: + return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v) + } +} + +func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *oidc_pb.GetDeviceAuthorizationRequestRequest) (*oidc_pb.GetDeviceAuthorizationRequestResponse, error) { + deviceRequest, err := s.query.DeviceAuthRequestByUserCode(ctx, req.GetUserCode()) + if err != nil { + return nil, err + } + encrypted, err := s.encryption.Encrypt([]byte(deviceRequest.DeviceCode)) + if err != nil { + return nil, err + } + return &oidc_pb.GetDeviceAuthorizationRequestResponse{ + DeviceAuthorizationRequest: &oidc_pb.DeviceAuthorizationRequest{ + Id: base64.RawURLEncoding.EncodeToString(encrypted), + ClientId: deviceRequest.ClientID, + Scope: deviceRequest.Scopes, + AppName: deviceRequest.AppName, + ProjectName: deviceRequest.ProjectName, + }, + }, nil +} + +func (s *Server) AuthorizeOrDenyDeviceAuthorization(ctx context.Context, req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest) (*oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse, error) { + deviceCode, err := s.deviceCodeFromID(req.GetDeviceAuthorizationId()) + if err != nil { + return nil, err + } + switch req.GetDecision().(type) { + case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session: + _, err = s.command.ApproveDeviceAuthWithSession(ctx, deviceCode, req.GetSession().GetSessionId(), req.GetSession().GetSessionToken()) + case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny: + _, err = s.command.CancelDeviceAuth(ctx, deviceCode, domain.DeviceAuthCanceledDenied) + } + if err != nil { + return nil, err + } + return &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{}, nil +} + func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest { pba := &oidc_pb.AuthRequest{ Id: a.ID, @@ -87,17 +136,6 @@ func (s *Server) checkPermission(ctx context.Context, clientID string, userID st return nil } -func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) { - switch v := req.GetCallbackKind().(type) { - case *oidc_pb.CreateCallbackRequest_Error: - return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error) - case *oidc_pb.CreateCallbackRequest_Session: - return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session) - default: - return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v) - } -} - func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) { details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError())) if err != nil { @@ -120,7 +158,11 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str return nil, err } authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} - ctx = op.ContextWithIssuer(ctx, http.DomainContext(ctx).Origin()) + issuer := authReq.Issuer + if issuer == "" { + issuer = http_utils.DomainContext(ctx).Origin() + } + ctx = op.ContextWithIssuer(ctx, issuer) var callback string if aar.ResponseType == domain.OIDCResponseTypeCode { callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) @@ -215,3 +257,11 @@ func errorReasonToOIDC(reason oidc_pb.ErrorReason) string { return "server_error" } } + +func (s *Server) deviceCodeFromID(deviceAuthID string) (string, error) { + decoded, err := base64.RawURLEncoding.DecodeString(deviceAuthID) + if err != nil { + return "", err + } + return s.encryption.DecryptString(decoded, s.encryption.EncryptionKeyID()) +} diff --git a/internal/api/grpc/oidc/v2/server.go b/internal/api/grpc/oidc/v2/server.go index 28c7134904..99234ee3d7 100644 --- a/internal/api/grpc/oidc/v2/server.go +++ b/internal/api/grpc/oidc/v2/server.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/query" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) @@ -20,6 +21,7 @@ type Server struct { op *oidc.Server externalSecure bool + encryption crypto.EncryptionAlgorithm } type Config struct{} @@ -29,12 +31,14 @@ func CreateServer( query *query.Queries, op *oidc.Server, externalSecure bool, + encryption crypto.EncryptionAlgorithm, ) *Server { return &Server{ command: command, query: query, op: op, externalSecure: externalSecure, + encryption: encryption, } } diff --git a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go index 1d2a6d2671..d7d746e2d0 100644 --- a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go @@ -76,19 +76,22 @@ func TestServer_GetAuthRequest(t *testing.T) { now, authRequestID, err := tt.dep() require.NoError(t, err) - got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ - AuthRequestId: authRequestID, - }) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - authRequest := got.GetAuthRequest() - assert.NotNil(t, authRequest) - assert.Equal(t, authRequestID, authRequest.GetId()) - assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) - assert.Contains(t, authRequest.GetScope(), "openid") + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{ + AuthRequestId: authRequestID, + }) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + authRequest := got.GetAuthRequest() + assert.NotNil(ttt, authRequest) + assert.Equal(ttt, authRequestID, authRequest.GetId()) + assert.WithinRange(ttt, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) + assert.Contains(ttt, authRequest.GetScope(), "openid") + }, retryDuration, tick) }) } } diff --git a/internal/api/grpc/org/v2/integration_test/query_test.go b/internal/api/grpc/org/v2/integration_test/query_test.go index 188aeddf9f..86a2bd312b 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -26,6 +26,16 @@ type orgAttr struct { Details *object.Details } +func createOrganization(ctx context.Context, name string) orgAttr { + orgResp := Instance.CreateOrganization(ctx, name, gofakeit.Email()) + orgResp.Details.CreationDate = orgResp.Details.ChangeDate + return orgAttr{ + ID: orgResp.GetOrganizationId(), + Name: name, + Details: orgResp.GetDetails(), + } +} + func TestServer_ListOrganizations(t *testing.T) { type args struct { ctx context.Context @@ -63,6 +73,7 @@ func TestServer_ListOrganizations(t *testing.T) { State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, Details: &object.Details{ Sequence: Instance.DefaultOrg.Details.Sequence, + CreationDate: Instance.DefaultOrg.Details.CreationDate, ChangeDate: Instance.DefaultOrg.Details.ChangeDate, ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, }, @@ -85,12 +96,7 @@ func TestServer_ListOrganizations(t *testing.T) { prefix := fmt.Sprintf("ListOrgs-%s", gofakeit.AppName()) for i := 0; i < count; i++ { name := prefix + strconv.Itoa(i) - orgResp := Instance.CreateOrganization(ctx, name, gofakeit.Email()) - orgs[i] = orgAttr{ - ID: orgResp.GetOrganizationId(), - Name: name, - Details: orgResp.GetDetails(), - } + orgs[i] = createOrganization(ctx, name) } request.Queries = []*org.SearchQuery{ OrganizationNamePrefixQuery(prefix), @@ -140,6 +146,7 @@ func TestServer_ListOrganizations(t *testing.T) { Name: Instance.DefaultOrg.Name, Details: &object.Details{ Sequence: Instance.DefaultOrg.Details.Sequence, + CreationDate: Instance.DefaultOrg.Details.CreationDate, ChangeDate: Instance.DefaultOrg.Details.ChangeDate, ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, }, @@ -172,6 +179,7 @@ func TestServer_ListOrganizations(t *testing.T) { Name: Instance.DefaultOrg.Name, Details: &object.Details{ Sequence: Instance.DefaultOrg.Details.Sequence, + CreationDate: Instance.DefaultOrg.Details.CreationDate, ChangeDate: Instance.DefaultOrg.Details.ChangeDate, ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, }, @@ -204,6 +212,7 @@ func TestServer_ListOrganizations(t *testing.T) { Name: Instance.DefaultOrg.Name, Details: &object.Details{ Sequence: Instance.DefaultOrg.Details.Sequence, + CreationDate: Instance.DefaultOrg.Details.CreationDate, ChangeDate: Instance.DefaultOrg.Details.ChangeDate, ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, }, @@ -221,14 +230,9 @@ func TestServer_ListOrganizations(t *testing.T) { func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { orgs := make([]orgAttr, 1) name := fmt.Sprintf("ListOrgs-%s", gofakeit.AppName()) - orgResp := Instance.CreateOrganization(ctx, name, gofakeit.Email()) - orgs[0] = orgAttr{ - ID: orgResp.GetOrganizationId(), - Name: name, - Details: orgResp.GetDetails(), - } + orgs[0] = createOrganization(ctx, name) domain := gofakeit.DomainName() - _, err := Instance.Client.Mgmt.AddOrgDomain(integration.SetOrgID(ctx, orgResp.GetOrganizationId()), &management.AddOrgDomainRequest{ + _, err := Instance.Client.Mgmt.AddOrgDomain(integration.SetOrgID(ctx, orgs[0].ID), &management.AddOrgDomainRequest{ Domain: domain, }) if err != nil { @@ -262,18 +266,19 @@ func TestServer_ListOrganizations(t *testing.T) { }, func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { name := gofakeit.Name() - orgResp := Instance.CreateOrganization(ctx, name, gofakeit.Email()) - deactivateOrgResp := Instance.DeactivateOrganization(ctx, orgResp.GetOrganizationId()) + orgResp := createOrganization(ctx, name) + deactivateOrgResp := Instance.DeactivateOrganization(ctx, orgResp.ID) request.Queries = []*org.SearchQuery{ - OrganizationIdQuery(orgResp.GetOrganizationId()), + OrganizationIdQuery(orgResp.ID), OrganizationStateQuery(org.OrganizationState_ORGANIZATION_STATE_INACTIVE), } return []orgAttr{{ - ID: orgResp.GetOrganizationId(), + ID: orgResp.ID, Name: name, Details: &object.Details{ ResourceOwner: deactivateOrgResp.GetDetails().GetResourceOwner(), Sequence: deactivateOrgResp.GetDetails().GetSequence(), + CreationDate: orgResp.Details.GetCreationDate(), ChangeDate: deactivateOrgResp.GetDetails().GetChangeDate(), }, }}, nil @@ -317,6 +322,7 @@ func TestServer_ListOrganizations(t *testing.T) { Name: Instance.DefaultOrg.Name, Details: &object.Details{ Sequence: Instance.DefaultOrg.Details.Sequence, + CreationDate: Instance.DefaultOrg.Details.ChangeDate, ChangeDate: Instance.DefaultOrg.Details.ChangeDate, ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, }, @@ -414,6 +420,7 @@ func TestServer_ListOrganizations(t *testing.T) { Name: Instance.DefaultOrg.Name, Details: &object.Details{ Sequence: Instance.DefaultOrg.Details.Sequence, + CreationDate: Instance.DefaultOrg.Details.ChangeDate, ChangeDate: Instance.DefaultOrg.Details.ChangeDate, ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, }, diff --git a/internal/api/grpc/org/v2/query.go b/internal/api/grpc/org/v2/query.go index f07fb71d20..27f279d40e 100644 --- a/internal/api/grpc/org/v2/query.go +++ b/internal/api/grpc/org/v2/query.go @@ -129,6 +129,7 @@ func organizationToPb(organization *query.Org) *org.Organization { Sequence: organization.Sequence, EventDate: organization.ChangeDate, ResourceOwner: organization.ResourceOwner, + CreationDate: organization.CreationDate, }), State: orgStateToPb(organization.State), } diff --git a/internal/api/grpc/project/application.go b/internal/api/grpc/project/application.go index 573156e637..fc05013c53 100644 --- a/internal/api/grpc/project/application.go +++ b/internal/api/grpc/project/application.go @@ -85,7 +85,8 @@ func loginVersionToPb(version domain.LoginVersion, baseURI *string) *app_pb.Logi func AppSAMLConfigToPb(app *query.SAMLApp) app_pb.AppConfig { return &app_pb.App_SamlConfig{ SamlConfig: &app_pb.SAMLConfig{ - Metadata: &app_pb.SAMLConfig_MetadataXml{MetadataXml: app.Metadata}, + Metadata: &app_pb.SAMLConfig_MetadataXml{MetadataXml: app.Metadata}, + LoginVersion: loginVersionToPb(app.LoginVersion, app.LoginBaseURI), }, } } diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go deleted file mode 100644 index 7aff6afb3f..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go +++ /dev/null @@ -1,410 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "reflect" - "testing" - "time" - - "github.com/brianvoe/gofakeit/v6" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" -) - -func TestServer_ExecutionTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - - fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" - - tests := []struct { - name string - ctx context.Context - dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (func(), error) - clean func(context.Context) - req *action.GetTargetRequest - want *action.GetTargetResponse - wantErr bool - }{ - { - name: "GetTarget, request and response, ok", - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { - - orgID := instance.DefaultOrg.Id - projectID := "" - userID := instance.Users.Get(integration.UserTypeIAMOwner).ID - - // create target for target changes - targetCreatedName := gofakeit.Name() - targetCreatedURL := "https://nonexistent" - - targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) - - // request received by target - wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} - changedRequest := &action.GetTargetRequest{Id: targetCreated.GetDetails().GetId()} - // replace original request with different targetID - urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest) - - targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, false) - - waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId())) - - // expected response from the GetTarget - expectedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Config: &action.Target{ - Name: targetCreatedName, - Endpoint: targetCreatedURL, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - Details: targetCreated.GetDetails(), - }, - } - // has to be set separately because of the pointers - response.Target = &action.GetTarget{ - Details: targetCreated.GetDetails(), - Config: &action.Target{ - Name: targetCreatedName, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - Endpoint: targetCreatedURL, - }, - } - - // content for partial update - changedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Id: targetCreated.GetDetails().GetId(), - }, - }, - } - - // response received by target - wantResponse := &middleware.ContextInfoResponse{ - FullMethod: fullMethod, - InstanceID: instance.ID(), - OrgID: orgID, - ProjectID: projectID, - UserID: userID, - Request: changedRequest, - Response: expectedResponse, - } - // after request with different targetID, return changed response - targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse) - - targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, false) - waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetDetails().GetId())) - return func() { - closeRequest() - closeResponse() - }, nil - }, - clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) - instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) - }, - req: &action.GetTargetRequest{ - Id: "something", - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Id: "changed", - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - }, - { - name: "GetTarget, request, interrupt", - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { - - fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" - orgID := instance.DefaultOrg.Id - projectID := "" - userID := instance.Users.Get(integration.UserTypeIAMOwner).ID - - // request received by target - wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} - urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetRequest{Id: "notchanged"}) - - targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId())) - // GetTarget with used target - request.Id = targetRequest.GetDetails().GetId() - return func() { - closeRequest() - }, nil - }, - clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) - }, - req: &action.GetTargetRequest{}, - wantErr: true, - }, - { - name: "GetTarget, response, interrupt", - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { - - fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" - orgID := instance.DefaultOrg.Id - projectID := "" - userID := instance.Users.Get(integration.UserTypeIAMOwner).ID - - // create target for target changes - targetCreatedName := gofakeit.Name() - targetCreatedURL := "https://nonexistent" - - targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) - - // GetTarget with used target - request.Id = targetCreated.GetDetails().GetId() - - // expected response from the GetTarget - expectedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: targetCreated.GetDetails(), - Config: &action.Target{ - Name: targetCreatedName, - Endpoint: targetCreatedURL, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - } - // content for partial update - changedResponse := &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Id: "changed", - }, - }, - } - - // response received by target - wantResponse := &middleware.ContextInfoResponse{ - FullMethod: fullMethod, - InstanceID: instance.ID(), - OrgID: orgID, - ProjectID: projectID, - UserID: userID, - Request: request, - Response: expectedResponse, - } - // after request with different targetID, return changed response - targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse) - - targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, true) - waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetDetails().GetId())) - return func() { - closeResponse() - }, nil - }, - clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) - }, - req: &action.GetTargetRequest{}, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - close, err := tt.dep(tt.ctx, tt.req, tt.want) - require.NoError(t, err) - defer close() - } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.GetTarget(tt.ctx, tt.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - require.NoError(ttt, err) - - integration.AssertResourceDetails(ttt, tt.want.GetTarget().GetDetails(), got.GetTarget().GetDetails()) - tt.want.Target.Details = got.GetTarget().GetDetails() - assert.EqualExportedValues(ttt, tt.want.GetTarget().GetConfig(), got.GetTarget().GetConfig()) - - }, retryDuration, tick, "timeout waiting for expected execution result") - - if tt.clean != nil { - tt.clean(tt.ctx) - } - }) - } -} - -func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []*action.ExecutionTargetType) { - instance.SetExecution(ctx, t, condition, targets) - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.SearchExecutions(ctx, &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{ - {Filter: &action.ExecutionSearchFilter_InConditionsFilter{ - InConditionsFilter: &action.InConditionsFilter{Conditions: []*action.Condition{condition}}, - }}, - }, - }) - if !assert.NoError(ttt, err) { - return - } - if !assert.Len(ttt, got.GetResult(), 1) { - return - } - gotTargets := got.GetResult()[0].GetExecution().GetTargets() - // always first check length, otherwise its failed anyway - if assert.Len(ttt, gotTargets, len(targets)) { - for i := range targets { - assert.EqualExportedValues(ttt, targets[i].GetType(), gotTargets[i].GetType()) - } - } - }, retryDuration, tick, "timeout waiting for expected execution result") - return -} - -func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { - resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt) - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.SearchTargets(ctx, &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{ - {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetDetails().GetId()}}, - }}, - }, - }) - if !assert.NoError(ttt, err) { - return - } - if !assert.Len(ttt, got.GetResult(), 1) { - return - } - config := got.GetResult()[0].GetConfig() - assert.Equal(ttt, config.GetEndpoint(), endpoint) - switch ty { - case domain.TargetTypeWebhook: - if !assert.NotNil(ttt, config.GetRestWebhook()) { - return - } - assert.Equal(ttt, interrupt, config.GetRestWebhook().GetInterruptOnError()) - case domain.TargetTypeAsync: - assert.NotNil(ttt, config.GetRestAsync()) - case domain.TargetTypeCall: - if !assert.NotNil(ttt, config.GetRestCall()) { - return - } - assert.Equal(ttt, interrupt, config.GetRestCall().GetInterruptOnError()) - } - }, retryDuration, tick, "timeout waiting for expected execution result") - return resp -} - -func conditionRequestFullMethod(fullMethod string) *action.Condition { - return &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: fullMethod, - }, - }, - }, - } -} - -func conditionResponseFullMethod(fullMethod string) *action.Condition { - return &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: fullMethod, - }, - }, - }, - } -} - -func testServerCall( - reqBody interface{}, - sleep time.Duration, - statusCode int, - respBody interface{}, -) (string, func()) { - handler := func(w http.ResponseWriter, r *http.Request) { - data, err := json.Marshal(reqBody) - if err != nil { - http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) - return - } - - sentBody, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) - return - } - if !reflect.DeepEqual(data, sentBody) { - http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) - return - } - if statusCode != http.StatusOK { - http.Error(w, "error, statusCode", statusCode) - return - } - - time.Sleep(sleep) - - w.Header().Set("Content-Type", "application/json") - resp, err := json.Marshal(respBody) - if err != nil { - http.Error(w, "error", http.StatusInternalServerError) - return - } - if _, err := io.WriteString(w, string(resp)); err != nil { - http.Error(w, "error", http.StatusInternalServerError) - return - } - } - - server := httptest.NewServer(http.HandlerFunc(handler)) - - return server.URL, server.Close -} diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go deleted file mode 100644 index b56efd6b99..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go +++ /dev/null @@ -1,812 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" -) - -func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { - return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}} -} - -func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType { - return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}} -} - -func TestServer_SetExecution_Request(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.NotExistingService/List", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "service, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "all, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Request_Include(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - executionCond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - executionCond, - executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - ) - - circularExecutionService := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - circularExecutionService, - executionTargetsSingleInclude(executionCond), - ) - circularExecutionMethod := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - } - instance.SetExecution(isolatedIAMOwnerCTX, t, - circularExecutionMethod, - executionTargetsSingleInclude(circularExecutionService), - ) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "method, circular error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: circularExecutionService, - Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(circularExecutionMethod), - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(executionCond), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "service, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(executionCond), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Response(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2beta.NotExistingService/List", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "service, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "all, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{ - All: true, - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Event(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - /* - //TODO event existing check - - { - name: "event, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - Targets: []string{targetResp.GetId()}, - }, - wantErr: true, - }, - */ - { - name: "event, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - /* - // TODO: - - { - name: "group, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - Targets: []string{targetResp.GetId()}, - }, - wantErr: true, - }, - */ - { - name: "group, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "all, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Function(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "function, not existing", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "xxx"}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - wantErr: true, - }, - { - name: "function, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - want: &action.SetExecutionResponse{ - Details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertResourceDetails(t, tt.want.Details, got.Details) - - // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go deleted file mode 100644 index aa748ac4d8..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go +++ /dev/null @@ -1,905 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/brianvoe/gofakeit/v6" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" -) - -func TestServer_GetTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - type args struct { - ctx context.Context - dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error - req *action.GetTargetRequest - } - tests := []struct { - name string - args args - want *action.GetTargetResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.GetTargetRequest{}, - }, - wantErr: true, - }, - { - name: "not found", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.GetTargetRequest{Id: "notexisting"}, - }, - wantErr: true, - }, - { - name: "get, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "get, async, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "get, webhook interruptOnError, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "get, call, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "get, call interruptOnError, ok", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true) - request.Id = resp.GetDetails().GetId() - response.Target.Config.Name = name - response.Target.Details = resp.GetDetails() - response.Target.SigningKey = resp.GetSigningKey() - return nil - }, - req: &action.GetTargetRequest{}, - }, - want: &action.GetTargetResponse{ - Target: &action.GetTarget{ - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, 2*time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := instance.Client.ActionV3Alpha.GetTarget(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(ttt, err, "Error: "+err.Error()) - return - } - if !assert.NoError(ttt, err) { - return - } - - wantTarget := tt.want.GetTarget() - gotTarget := got.GetTarget() - integration.AssertResourceDetails(ttt, wantTarget.GetDetails(), gotTarget.GetDetails()) - assert.EqualExportedValues(ttt, wantTarget.GetConfig(), gotTarget.GetConfig()) - assert.Equal(ttt, wantTarget.GetSigningKey(), gotTarget.GetSigningKey()) - }, retryDuration, tick, "timeout waiting for expected target result") - }) - } -} - -func TestServer_ListTargets(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - type args struct { - ctx context.Context - dep func(context.Context, *action.SearchTargetsRequest, *action.SearchTargetsResponse) error - req *action.SearchTargetsRequest - } - tests := []struct { - name string - args args - want *action.SearchTargetsResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SearchTargetsRequest{}, - }, - wantErr: true, - }, - { - name: "list, not found", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{ - {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{ - TargetIds: []string{"notfound"}, - }, - }, - }, - }, - }, - }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 0, - AppliedLimit: 100, - }, - Result: []*action.GetTarget{}, - }, - }, - { - name: "list single id", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{ - TargetIds: []string{resp.GetDetails().GetId()}, - }, - } - response.Details.Timestamp = resp.GetDetails().GetChanged() - - response.Result[0].Details = resp.GetDetails() - response.Result[0].Config.Name = name - return nil - }, - req: &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{{}}, - }, - }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetTarget{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - }, { - name: "list single name", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { - name := gofakeit.Name() - resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{ - TargetNameFilter: &action.TargetNameFilter{ - TargetName: name, - }, - } - response.Details.Timestamp = resp.GetDetails().GetChanged() - - response.Result[0].Details = resp.GetDetails() - response.Result[0].Config.Name = name - return nil - }, - req: &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{{}}, - }, - }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetTarget{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - }, - { - name: "list multiple id", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { - name1 := gofakeit.Name() - name2 := gofakeit.Name() - name3 := gofakeit.Name() - resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false) - resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true) - resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false) - request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ - InTargetIdsFilter: &action.InTargetIDsFilter{ - TargetIds: []string{resp1.GetDetails().GetId(), resp2.GetDetails().GetId(), resp3.GetDetails().GetId()}, - }, - } - response.Details.Timestamp = resp3.GetDetails().GetChanged() - - response.Result[0].Details = resp1.GetDetails() - response.Result[0].Config.Name = name1 - response.Result[1].Details = resp2.GetDetails() - response.Result[1].Config.Name = name2 - response.Result[2].Details = resp3.GetDetails() - response.Result[2].Config.Name = name3 - return nil - }, - req: &action.SearchTargetsRequest{ - Filters: []*action.TargetSearchFilter{{}}, - }, - }, - want: &action.SearchTargetsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 3, - AppliedLimit: 100, - }, - Result: []*action.GetTarget{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - Config: &action.Target{ - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, listErr := instance.Client.ActionV3Alpha.SearchTargets(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, listErr, "Error: "+listErr.Error()) - return - } - require.NoError(ttt, listErr) - - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - for i := range tt.want.Result { - integration.AssertResourceDetails(ttt, tt.want.Result[i].GetDetails(), got.Result[i].GetDetails()) - assert.EqualExportedValues(ttt, tt.want.Result[i].GetConfig(), got.Result[i].GetConfig()) - assert.NotEmpty(ttt, got.Result[i].GetSigningKey()) - } - } - integration.AssertResourceListDetails(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected execution result") - }) - } -} - -func TestServer_SearchExecutions(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) - - type args struct { - ctx context.Context - dep func(context.Context, *action.SearchExecutionsRequest, *action.SearchExecutionsResponse) error - req *action.SearchExecutionsRequest - } - tests := []struct { - name string - args args - want *action.SearchExecutionsResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.SearchExecutionsRequest{}, - }, - wantErr: true, - }, - { - name: "list request single condition", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - resp := instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId())) - - response.Details.Timestamp = resp.GetDetails().GetChanged() - // Set expected response with used values for SetExecution - response.Result[0].Details = resp.GetDetails() - response.Result[0].Condition = cond - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_InConditionsFilter{ - InConditionsFilter: &action.InConditionsFilter{ - Conditions: []*action.Condition{{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }}, - }, - }, - }}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), - }, - }, - }, - }, - }, - { - name: "list request single target", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) - // add target as Filter to the request - request.Filters[0] = &action.ExecutionSearchFilter{ - Filter: &action.ExecutionSearchFilter_TargetFilter{ - TargetFilter: &action.TargetFilter{ - TargetId: target.GetDetails().GetId(), - }, - }, - } - cond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/UpdateAction", - }, - }, - }, - } - targets := executionTargetsSingleTarget(target.GetDetails().GetId()) - resp := instance.SetExecution(ctx, t, cond, targets) - - response.Details.Timestamp = resp.GetDetails().GetChanged() - - response.Result[0].Details = resp.GetDetails() - response.Result[0].Condition = cond - response.Result[0].Execution.Targets = targets - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{}}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - Condition: &action.Condition{}, - Execution: &action.Execution{ - Targets: executionTargetsSingleTarget(""), - }, - }, - }, - }, - }, { - name: "list request single include", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - cond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/GetAction", - }, - }, - }, - } - instance.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId())) - request.Filters[0].GetIncludeFilter().Include = cond - - includeCond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/ListActions", - }, - }, - }, - } - includeTargets := executionTargetsSingleInclude(cond) - resp2 := instance.SetExecution(ctx, t, includeCond, includeTargets) - - response.Details.Timestamp = resp2.GetDetails().GetChanged() - - response.Result[0].Details = resp2.GetDetails() - response.Result[0].Condition = includeCond - response.Result[0].Execution = &action.Execution{ - Targets: includeTargets, - } - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_IncludeFilter{ - IncludeFilter: &action.IncludeFilter{}, - }, - }}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 1, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Created: timestamppb.Now(), - Changed: timestamppb.Now(), - }, - }, - }, - }, - }, - { - name: "list multiple conditions", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - - cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - targets1 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) - resp1 := instance.SetExecution(ctx, t, cond1, targets1) - response.Result[0].Details = resp1.GetDetails() - response.Result[0].Condition = cond1 - response.Result[0].Execution = &action.Execution{ - Targets: targets1, - } - - cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] - targets2 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) - resp2 := instance.SetExecution(ctx, t, cond2, targets2) - response.Result[1].Details = resp2.GetDetails() - response.Result[1].Condition = cond2 - response.Result[1].Execution = &action.Execution{ - Targets: targets2, - } - - cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] - targets3 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) - resp3 := instance.SetExecution(ctx, t, cond3, targets3) - response.Result[2].Details = resp3.GetDetails() - response.Result[2].Condition = cond3 - response.Result[2].Execution = &action.Execution{ - Targets: targets3, - } - response.Details.Timestamp = resp3.GetDetails().GetChanged() - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_InConditionsFilter{ - InConditionsFilter: &action.InConditionsFilter{ - Conditions: []*action.Condition{ - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/CreateSession", - }, - }, - }, - }, - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/SetSession", - }, - }, - }, - }, - }, - }, - }, - }}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 3, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - { - Details: &resource_object.Details{ - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}, - }, - }, { - Details: &resource_object.Details{ - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}, - }, - }, { - Details: &resource_object.Details{ - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}, - }, - }, - }, - }, - }, - { - name: "list multiple conditions all types", - args: args{ - ctx: isolatedIAMOwnerCTX, - dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { - targets := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) - for i, cond := range request.Filters[0].GetInConditionsFilter().GetConditions() { - resp := instance.SetExecution(ctx, t, cond, targets) - response.Result[i].Details = resp.GetDetails() - response.Result[i].Condition = cond - response.Result[i].Execution = &action.Execution{ - Targets: targets, - } - // filled with info of last sequence - response.Details.Timestamp = resp.GetDetails().GetChanged() - } - - return nil - }, - req: &action.SearchExecutionsRequest{ - Filters: []*action.ExecutionSearchFilter{{ - Filter: &action.ExecutionSearchFilter_InConditionsFilter{ - InConditionsFilter: &action.InConditionsFilter{ - Conditions: []*action.Condition{ - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}}, - }, - }, - }, - }}, - }, - }, - want: &action.SearchExecutionsResponse{ - Details: &resource_object.ListDetails{ - TotalResult: 10, - AppliedLimit: 100, - }, - Result: []*action.GetExecution{ - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instance.ID()}}}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, listErr := instance.Client.ActionV3Alpha.SearchExecutions(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, listErr, "Error: "+listErr.Error()) - return - } - require.NoError(ttt, listErr) - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - for i := range tt.want.Result { - // as not sorted, all elements have to be checked - // workaround as oneof elements can only be checked with assert.EqualExportedValues() - if j, found := containExecution(got.Result, tt.want.Result[i]); found { - integration.AssertResourceDetails(ttt, tt.want.Result[i].GetDetails(), got.Result[j].GetDetails()) - got.Result[j].Details = tt.want.Result[i].GetDetails() - assert.EqualExportedValues(ttt, tt.want.Result[i], got.Result[j]) - } - } - } - integration.AssertResourceListDetails(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected execution result") - }) - } -} - -func containExecution(executionList []*action.GetExecution, execution *action.GetExecution) (int, bool) { - for i, exec := range executionList { - if reflect.DeepEqual(exec.Details, execution.Details) { - return i, true - } - } - return 0, false -} diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go deleted file mode 100644 index bc8e43eafc..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/server_test.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "os" - "testing" - "time" - - "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" -) - -var ( - CTX context.Context -) - -func TestMain(m *testing.M) { - os.Exit(func() int { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) - defer cancel() - CTX = ctx - return m.Run() - }()) -} - -func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) { - ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - require.NoError(t, err) - if f.Actions.GetEnabled() { - return - } - _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ - Actions: gu.Ptr(true), - }) - require.NoError(t, err) - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) - require.EventuallyWithT(t, - func(ttt *assert.CollectT) { - f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - assert.NoError(ttt, err) - assert.True(ttt, f.Actions.GetEnabled()) - }, - retryDuration, - tick, - "timed out waiting for ensuring instance feature") - - retryDuration, tick = integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) - require.EventuallyWithT(t, - func(ttt *assert.CollectT) { - _, err := instance.Client.ActionV3Alpha.ListExecutionMethods(ctx, &action.ListExecutionMethodsRequest{}) - assert.NoError(ttt, err) - }, - retryDuration, - tick, - "timed out waiting for ensuring instance feature call") -} diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go deleted file mode 100644 index b5d1903ca6..0000000000 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go +++ /dev/null @@ -1,499 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "testing" - "time" - - "github.com/brianvoe/gofakeit/v6" - "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" -) - -func TestServer_CreateTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - tests := []struct { - name string - ctx context.Context - req *action.Target - want *resource_object.Details - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.Target{ - Name: gofakeit.Name(), - }, - wantErr: true, - }, - { - name: "empty name", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: "", - }, - wantErr: true, - }, - { - name: "empty type", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - TargetType: nil, - }, - wantErr: true, - }, - { - name: "empty webhook url", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - }, - wantErr: true, - }, - { - name: "empty request response url", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{}, - }, - }, - wantErr: true, - }, - { - name: "empty timeout", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - Timeout: nil, - }, - wantErr: true, - }, - { - name: "async, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - { - name: "webhook, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - { - name: "webhook, interrupt on error, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - { - name: "call, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - - { - name: "call, interruptOnError, ok", - ctx: isolatedIAMOwnerCTX, - req: &action.Target{ - Name: gofakeit.Name(), - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := instance.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req}) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - integration.AssertResourceDetails(t, tt.want, got.Details) - assert.NotEmpty(t, got.GetSigningKey()) - } - }) - } -} - -func TestServer_PatchTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - type args struct { - ctx context.Context - req *action.PatchTargetRequest - } - type want struct { - details *resource_object.Details - signingKey bool - } - tests := []struct { - name string - prepare func(request *action.PatchTargetRequest) error - args args - want want - wantErr bool - }{ - { - name: "missing permission", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Name: gu.Ptr(gofakeit.Name()), - }, - }, - }, - wantErr: true, - }, - { - name: "not existing", - prepare: func(request *action.PatchTargetRequest) error { - request.Id = "notexisting" - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Name: gu.Ptr(gofakeit.Name()), - }, - }, - }, - wantErr: true, - }, - { - name: "change name, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Name: gu.Ptr(gofakeit.Name()), - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "regenerate signingkey, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - ExpirationSigningKey: durationpb.New(0 * time.Second), - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - signingKey: true, - }, - }, - { - name: "change type, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - TargetType: &action.PatchTarget_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "change url, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Endpoint: gu.Ptr("https://example.com/hooks/new"), - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "change timeout, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - Timeout: durationpb.New(20 * time.Second), - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - { - name: "change type async, ok", - prepare: func(request *action.PatchTargetRequest) error { - targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId() - request.Id = targetID - return nil - }, - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &action.PatchTargetRequest{ - Target: &action.PatchTarget{ - TargetType: &action.PatchTarget_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - }, - }, - }, - want: want{ - details: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.prepare(tt.args.req) - require.NoError(t, err) - // We want to have the same response no matter how often we call the function - instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req) - got, err := instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - integration.AssertResourceDetails(t, tt.want.details, got.Details) - if tt.want.signingKey { - assert.NotEmpty(t, got.SigningKey) - } - } - }) - } -} - -func TestServer_DeleteTarget(t *testing.T) { - instance := integration.NewInstance(CTX) - ensureFeatureEnabled(t, instance) - iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - target := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false) - tests := []struct { - name string - ctx context.Context - req *action.DeleteTargetRequest - want *resource_object.Details - wantErr bool - }{ - { - name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &action.DeleteTargetRequest{ - Id: target.GetDetails().GetId(), - }, - wantErr: true, - }, - { - name: "empty id", - ctx: iamOwnerCtx, - req: &action.DeleteTargetRequest{ - Id: "", - }, - wantErr: true, - }, - { - name: "delete target", - ctx: iamOwnerCtx, - req: &action.DeleteTargetRequest{ - Id: target.GetDetails().GetId(), - }, - want: &resource_object.Details{ - Changed: timestamppb.Now(), - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := instance.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req) - if tt.wantErr { - assert.Error(t, err) - return - } else { - assert.NoError(t, err) - integration.AssertResourceDetails(t, tt.want, got.Details) - } - }) - } -} diff --git a/internal/api/grpc/resources/webkey/v3/webkey_converter.go b/internal/api/grpc/resources/webkey/v3/webkey_converter.go deleted file mode 100644 index b460775dd5..0000000000 --- a/internal/api/grpc/resources/webkey/v3/webkey_converter.go +++ /dev/null @@ -1,173 +0,0 @@ -package webkey - -import ( - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" -) - -func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig { - switch config := req.GetKey().GetConfig().(type) { - case *webkey.WebKey_Rsa: - return webKeyRSAConfigToCrypto(config.Rsa) - case *webkey.WebKey_Ecdsa: - return webKeyECDSAConfigToCrypto(config.Ecdsa) - case *webkey.WebKey_Ed25519: - return new(crypto.WebKeyED25519Config) - default: - return webKeyRSAConfigToCrypto(nil) - } -} - -func webKeyRSAConfigToCrypto(config *webkey.WebKeyRSAConfig) *crypto.WebKeyRSAConfig { - out := new(crypto.WebKeyRSAConfig) - - switch config.GetBits() { - case webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED: - out.Bits = crypto.RSABits2048 - case webkey.WebKeyRSAConfig_RSA_BITS_2048: - out.Bits = crypto.RSABits2048 - case webkey.WebKeyRSAConfig_RSA_BITS_3072: - out.Bits = crypto.RSABits3072 - case webkey.WebKeyRSAConfig_RSA_BITS_4096: - out.Bits = crypto.RSABits4096 - default: - out.Bits = crypto.RSABits2048 - } - - switch config.GetHasher() { - case webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED: - out.Hasher = crypto.RSAHasherSHA256 - case webkey.WebKeyRSAConfig_RSA_HASHER_SHA256: - out.Hasher = crypto.RSAHasherSHA256 - case webkey.WebKeyRSAConfig_RSA_HASHER_SHA384: - out.Hasher = crypto.RSAHasherSHA384 - case webkey.WebKeyRSAConfig_RSA_HASHER_SHA512: - out.Hasher = crypto.RSAHasherSHA512 - default: - out.Hasher = crypto.RSAHasherSHA256 - } - - return out -} - -func webKeyECDSAConfigToCrypto(config *webkey.WebKeyECDSAConfig) *crypto.WebKeyECDSAConfig { - out := new(crypto.WebKeyECDSAConfig) - - switch config.GetCurve() { - case webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED: - out.Curve = crypto.EllipticCurveP256 - case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256: - out.Curve = crypto.EllipticCurveP256 - case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384: - out.Curve = crypto.EllipticCurveP384 - case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512: - out.Curve = crypto.EllipticCurveP512 - default: - out.Curve = crypto.EllipticCurveP256 - } - - return out -} - -func webKeyDetailsListToPb(list []query.WebKeyDetails, instanceID string) []*webkey.GetWebKey { - out := make([]*webkey.GetWebKey, len(list)) - for i := range list { - out[i] = webKeyDetailsToPb(&list[i], instanceID) - } - return out -} - -func webKeyDetailsToPb(details *query.WebKeyDetails, instanceID string) *webkey.GetWebKey { - out := &webkey.GetWebKey{ - Details: resource_object.DomainToDetailsPb(&domain.ObjectDetails{ - ID: details.KeyID, - CreationDate: details.CreationDate, - EventDate: details.ChangeDate, - }, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), - State: webKeyStateToPb(details.State), - Config: &webkey.WebKey{}, - } - - switch config := details.Config.(type) { - case *crypto.WebKeyRSAConfig: - out.Config.Config = &webkey.WebKey_Rsa{ - Rsa: webKeyRSAConfigToPb(config), - } - case *crypto.WebKeyECDSAConfig: - out.Config.Config = &webkey.WebKey_Ecdsa{ - Ecdsa: webKeyECDSAConfigToPb(config), - } - case *crypto.WebKeyED25519Config: - out.Config.Config = &webkey.WebKey_Ed25519{ - Ed25519: new(webkey.WebKeyED25519Config), - } - } - - return out -} - -func webKeyStateToPb(state domain.WebKeyState) webkey.WebKeyState { - switch state { - case domain.WebKeyStateUnspecified: - return webkey.WebKeyState_STATE_UNSPECIFIED - case domain.WebKeyStateInitial: - return webkey.WebKeyState_STATE_INITIAL - case domain.WebKeyStateActive: - return webkey.WebKeyState_STATE_ACTIVE - case domain.WebKeyStateInactive: - return webkey.WebKeyState_STATE_INACTIVE - case domain.WebKeyStateRemoved: - return webkey.WebKeyState_STATE_REMOVED - default: - return webkey.WebKeyState_STATE_UNSPECIFIED - } -} - -func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.WebKeyRSAConfig { - out := new(webkey.WebKeyRSAConfig) - - switch config.Bits { - case crypto.RSABitsUnspecified: - out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED - case crypto.RSABits2048: - out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_2048 - case crypto.RSABits3072: - out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_3072 - case crypto.RSABits4096: - out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_4096 - } - - switch config.Hasher { - case crypto.RSAHasherUnspecified: - out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED - case crypto.RSAHasherSHA256: - out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA256 - case crypto.RSAHasherSHA384: - out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA384 - case crypto.RSAHasherSHA512: - out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA512 - } - - return out -} - -func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.WebKeyECDSAConfig { - out := new(webkey.WebKeyECDSAConfig) - - switch config.Curve { - case crypto.EllipticCurveUnspecified: - out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED - case crypto.EllipticCurveP256: - out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256 - case crypto.EllipticCurveP384: - out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384 - case crypto.EllipticCurveP512: - out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512 - } - - return out -} diff --git a/internal/api/grpc/saml/v2/saml.go b/internal/api/grpc/saml/v2/saml.go index 866846dfd7..43eae5feb1 100644 --- a/internal/api/grpc/saml/v2/saml.go +++ b/internal/api/grpc/saml/v2/saml.go @@ -4,9 +4,11 @@ import ( "context" "github.com/zitadel/logging" + "github.com/zitadel/saml/pkg/provider" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/saml" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -76,6 +78,11 @@ func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID str return nil, err } authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar} + responseIssuer := authReq.ResponseIssuer + if responseIssuer == "" { + responseIssuer = http_utils.DomainContext(ctx).Origin() + } + ctx = provider.ContextWithIssuer(ctx, responseIssuer) url, body, err := s.idp.CreateResponse(ctx, authReq) if err != nil { return nil, err diff --git a/internal/api/grpc/server/gateway.go b/internal/api/grpc/server/gateway.go index 43947917a2..ca7579ee89 100644 --- a/internal/api/grpc/server/gateway.go +++ b/internal/api/grpc/server/gateway.go @@ -28,6 +28,7 @@ import ( const ( mimeWildcard = "*/*" + UnknownPath = "UNKNOWN_PATH" ) var ( @@ -274,7 +275,11 @@ func grpcCredentials(tlsConfig *tls.Config) credentials.TransportCredentials { func setRequestURIPattern(ctx context.Context) { pattern, ok := runtime.HTTPPathPattern(ctx) if !ok { - return + // As all unmatched paths will be handled by the gateway, any request not matching a pattern, + // means there's no route to the path. + // To prevent high cardinality on metrics and tracing, we want to make sure we don't record + // the actual path as name (it will still be recorded explicitly in the span http info). + pattern = UnknownPath } span := trace.SpanFromContext(ctx) span.SetName(pattern) diff --git a/internal/api/grpc/server/middleware/access_interceptor.go b/internal/api/grpc/server/middleware/access_interceptor.go index 100264c3f5..f95c3225ed 100644 --- a/internal/api/grpc/server/middleware/access_interceptor.go +++ b/internal/api/grpc/server/middleware/access_interceptor.go @@ -20,7 +20,6 @@ func AccessStorageInterceptor(svc *logstore.Service[*record.AccessLog]) grpc.Una if !svc.Enabled() { return handler(ctx, req) } - reqMd, _ := metadata.FromIncomingContext(ctx) resp, handlerErr := handler(ctx, req) diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index 6eb326a59a..410b4b8abc 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -13,13 +13,13 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - return authorize(ctx, req, info, handler, verifier, authConfig) + return authorize(ctx, req, info, handler, verifier, systemUserPermissions, authConfig) } } -func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, authConfig authz.Config) (_ interface{}, err error) { +func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) (_ interface{}, err error) { authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod) if !needsToken { return handler(ctx, req) @@ -34,7 +34,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, } orgID, orgDomain := orgIDAndDomainFromRequest(authCtx, req) - ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, systemUserPermissions.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, info.FullMethod) if err != nil { return nil, err } diff --git a/internal/api/grpc/server/middleware/auth_interceptor_test.go b/internal/api/grpc/server/middleware/auth_interceptor_test.go index 3551d3e419..e098189445 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor_test.go +++ b/internal/api/grpc/server/middleware/auth_interceptor_test.go @@ -20,6 +20,7 @@ type authzRepoMock struct{} func (v *authzRepoMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { return "", "", "", "", "", nil } + func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) { return authz.Memberships{{ MemberType: authz.MemberTypeOrganization, @@ -31,9 +32,11 @@ func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) { return "", nil, nil } + func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) { return orgID, nil } + func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) { return "", "", nil } @@ -252,7 +255,7 @@ func Test_authorize(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig) + got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig, tt.args.authConfig) if (err != nil) != tt.res.wantErr { t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr) return diff --git a/internal/api/grpc/server/middleware/execution_interceptor.go b/internal/api/grpc/server/middleware/execution_interceptor.go index c309827d94..4aeea6c4da 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor.go +++ b/internal/api/grpc/server/middleware/execution_interceptor.go @@ -3,23 +3,20 @@ package middleware import ( "context" "encoding/json" - "strings" - "github.com/zitadel/logging" "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/query" - exec_repo "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" ) func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - requestTargets, responseTargets := queryTargets(ctx, queries, info.FullMethod) + requestTargets, responseTargets := execution.QueryExecutionTargetsForRequestAndResponse(ctx, queries, info.FullMethod) // call targets otherwise return req handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req) @@ -38,7 +35,7 @@ func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor { func executeTargetsForRequest(ctx context.Context, targets []execution.Target, fullMethod string, req interface{}) (_ interface{}, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() // if no targets are found, return without any calls if len(targets) == 0 { @@ -52,7 +49,7 @@ func executeTargetsForRequest(ctx context.Context, targets []execution.Target, f ProjectID: ctxData.ProjectID, OrgID: ctxData.OrgID, UserID: ctxData.UserID, - Request: req, + Request: Message{req.(proto.Message)}, } return execution.CallTargets(ctx, targets, info) @@ -60,7 +57,7 @@ func executeTargetsForRequest(ctx context.Context, targets []execution.Target, f func executeTargetsForResponse(ctx context.Context, targets []execution.Target, fullMethod string, req, resp interface{}) (_ interface{}, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() // if no targets are found, return without any calls if len(targets) == 0 { @@ -74,65 +71,38 @@ func executeTargetsForResponse(ctx context.Context, targets []execution.Target, ProjectID: ctxData.ProjectID, OrgID: ctxData.OrgID, UserID: ctxData.UserID, - Request: req, - Response: resp, + Request: Message{req.(proto.Message)}, + Response: Message{resp.(proto.Message)}, } return execution.CallTargets(ctx, targets, info) } -type ExecutionQueries interface { - TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*query.ExecutionTarget, err error) -} - -func queryTargets( - ctx context.Context, - queries ExecutionQueries, - fullMethod string, -) ([]execution.Target, []execution.Target) { - ctx, span := tracing.NewSpan(ctx) - defer span.End() - - targets, err := queries.TargetsByExecutionIDs(ctx, - idsForFullMethod(fullMethod, domain.ExecutionTypeRequest), - idsForFullMethod(fullMethod, domain.ExecutionTypeResponse), - ) - requestTargets := make([]execution.Target, 0, len(targets)) - responseTargets := make([]execution.Target, 0, len(targets)) - if err != nil { - logging.WithFields("fullMethod", fullMethod).WithError(err).Info("unable to query targets") - return requestTargets, responseTargets - } - - for _, target := range targets { - if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeRequest)) { - requestTargets = append(requestTargets, target) - } else if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeResponse)) { - responseTargets = append(responseTargets, target) - } - } - - return requestTargets, responseTargets -} - -func idsForFullMethod(fullMethod string, executionType domain.ExecutionType) []string { - return []string{exec_repo.ID(executionType, fullMethod), exec_repo.ID(executionType, serviceFromFullMethod(fullMethod)), exec_repo.IDAll(executionType)} -} - -func serviceFromFullMethod(s string) string { - parts := strings.Split(s, "/") - return parts[1] -} - var _ execution.ContextInfo = &ContextInfoRequest{} type ContextInfoRequest struct { - FullMethod string `json:"fullMethod,omitempty"` - InstanceID string `json:"instanceID,omitempty"` - OrgID string `json:"orgID,omitempty"` - ProjectID string `json:"projectID,omitempty"` - UserID string `json:"userID,omitempty"` - Request interface{} `json:"request,omitempty"` + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request Message `json:"request,omitempty"` +} + +type Message struct { + proto.Message +} + +func (r *Message) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r.Message) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *Message) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r.Message) } func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { @@ -144,26 +114,23 @@ func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { } func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error { - if !json.Valid(resp) { - return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON") - } - return json.Unmarshal(resp, c.Request) + return json.Unmarshal(resp, &c.Request) } func (c *ContextInfoRequest) GetContent() interface{} { - return c.Request + return c.Request.Message } var _ execution.ContextInfo = &ContextInfoResponse{} type ContextInfoResponse struct { - FullMethod string `json:"fullMethod,omitempty"` - InstanceID string `json:"instanceID,omitempty"` - OrgID string `json:"orgID,omitempty"` - ProjectID string `json:"projectID,omitempty"` - UserID string `json:"userID,omitempty"` - Request interface{} `json:"request,omitempty"` - Response interface{} `json:"response,omitempty"` + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request Message `json:"request,omitempty"` + Response Message `json:"response,omitempty"` } func (c *ContextInfoResponse) GetHTTPRequestBody() []byte { @@ -175,9 +142,9 @@ func (c *ContextInfoResponse) GetHTTPRequestBody() []byte { } func (c *ContextInfoResponse) SetHTTPResponseBody(resp []byte) error { - return json.Unmarshal(resp, c.Response) + return json.Unmarshal(resp, &c.Response) } func (c *ContextInfoResponse) GetContent() interface{} { - return c.Response + return c.Response.Message } diff --git a/internal/api/grpc/server/middleware/execution_interceptor_test.go b/internal/api/grpc/server/middleware/execution_interceptor_test.go index 6a5b74c5e4..281db4617a 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor_test.go +++ b/internal/api/grpc/server/middleware/execution_interceptor_test.go @@ -11,6 +11,9 @@ import ( "time" "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/execution" @@ -54,28 +57,28 @@ func (e *mockExecutionTarget) GetSigningKey() string { return e.SigningKey } -type mockContentRequest struct { - Content string -} - -func newMockContentRequest(content string) *mockContentRequest { - return &mockContentRequest{ - Content: content, +func newMockContentRequest(content string) proto.Message { + return &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "content": { + Kind: &structpb.Value_StringValue{StringValue: content}, + }, + }, } } func newMockContextInfoRequest(fullMethod, request string) *ContextInfoRequest { return &ContextInfoRequest{ FullMethod: fullMethod, - Request: newMockContentRequest(request), + Request: Message{Message: newMockContentRequest(request)}, } } func newMockContextInfoResponse(fullMethod, request, response string) *ContextInfoResponse { return &ContextInfoResponse{ FullMethod: fullMethod, - Request: newMockContentRequest(request), - Response: newMockContentRequest(response), + Request: Message{Message: newMockContentRequest(request)}, + Response: Message{Message: newMockContentRequest(response)}, } } @@ -591,7 +594,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { } else { assert.NoError(t, err) } - assert.Equal(t, tt.res.want, resp) + assert.EqualExportedValues(t, tt.res.want, resp) for _, closeF := range closeFuncs { closeF() @@ -632,7 +635,7 @@ func testServerCall( time.Sleep(sleep) w.Header().Set("Content-Type", "application/json") - resp, err := json.Marshal(respBody) + resp, err := protojson.Marshal(respBody.(proto.Message)) if err != nil { http.Error(w, "error", http.StatusInternalServerError) return @@ -723,7 +726,8 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { statusCode: http.StatusOK, }, }, - req: []byte{}, + req: newMockContentRequest(""), + resp: newMockContentRequest(""), }, res{ wantErr: true, @@ -790,7 +794,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { } else { assert.NoError(t, err) } - assert.Equal(t, tt.res.want, resp) + assert.EqualExportedValues(t, tt.res.want, resp) for _, closeF := range closeFuncs { closeF() diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 27b921b7d5..b686d3add9 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -36,6 +36,7 @@ type WithGatewayPrefix interface { func CreateServer( verifier authz.APITokenVerifier, + systemAuthz authz.Config, authConfig authz.Config, queries *query.Queries, externalDomain string, @@ -53,7 +54,7 @@ func CreateServer( middleware.AccessStorageInterceptor(accessSvc), middleware.ErrorHandler(), middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName), - middleware.AuthorizationInterceptor(verifier, authConfig), + middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig), middleware.TranslationHandler(), middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName), middleware.ExecutionHandler(queries), diff --git a/internal/api/grpc/session/v2/integration_test/server_test.go b/internal/api/grpc/session/v2/integration_test/server_test.go index 70e2146069..6ea2b4dbda 100644 --- a/internal/api/grpc/session/v2/integration_test/server_test.go +++ b/internal/api/grpc/session/v2/integration_test/server_test.go @@ -19,6 +19,7 @@ var ( CTX context.Context IAMOwnerCTX context.Context UserCTX context.Context + LoginCTX context.Context Instance *integration.Instance Client session.SessionServiceClient User *user.AddHumanUserResponse @@ -37,6 +38,7 @@ func TestMain(m *testing.M) { CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission) + LoginCTX = Instance.WithAuthorization(ctx, integration.UserTypeLogin) User = createFullUser(CTX) DeactivatedUser = createDeactivatedUser(CTX) LockedUser = createLockedUser(CTX) diff --git a/internal/api/grpc/session/v2/integration_test/session_test.go b/internal/api/grpc/session/v2/integration_test/session_test.go index 7622550b15..b9a060c749 100644 --- a/internal/api/grpc/session/v2/integration_test/session_test.go +++ b/internal/api/grpc/session/v2/integration_test/session_test.go @@ -21,6 +21,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/internal/integration/sink" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2" @@ -339,10 +340,9 @@ func TestServer_CreateSession_webauthn(t *testing.T) { verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified) } -/* func TestServer_CreateSession_successfulIntent(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) - createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ Search: &session.CheckUser_UserId{ @@ -354,8 +354,9 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) - intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id") - updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + require.NoError(t, err) + updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), Checks: &session.Checks{ IdpIntent: &session.CheckIDPIntent{ @@ -369,9 +370,10 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { } func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() - intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id") + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + require.NoError(t, err) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ @@ -390,11 +392,11 @@ func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { } func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() // successful intent without known / linked user idpUserID := "id" - intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "") // link the user (with info from intent) Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId()) @@ -418,7 +420,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { } func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -432,19 +434,18 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) - intentID := Instance.CreateIntent(t, CTX, idpID) + intent := Instance.CreateIntent(CTX, idpID) _, err = Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), Checks: &session.Checks{ IdpIntent: &session.CheckIDPIntent{ - IdpIntentId: intentID, + IdpIntentId: intent.GetIdpIntent().GetIdpIntentId(), IdpIntentToken: "false", }, }, }) require.Error(t, err) } -*/ func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) { resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index 7562d64350..08f19368ef 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -255,7 +255,7 @@ type userSearchByID struct { } func (u userSearchByID) search(ctx context.Context, q *query.Queries) (*query.User, error) { - return q.GetUserByID(ctx, true, u.id) + return q.GetUserByID(ctx, false, u.id) } type userSearchByLoginName struct { diff --git a/internal/api/grpc/session/v2beta/integration_test/session_test.go b/internal/api/grpc/session/v2beta/integration_test/session_test.go index 26d2291629..d0fc1179ef 100644 --- a/internal/api/grpc/session/v2beta/integration_test/session_test.go +++ b/internal/api/grpc/session/v2beta/integration_test/session_test.go @@ -21,6 +21,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/internal/integration/sink" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" @@ -339,9 +340,8 @@ func TestServer_CreateSession_webauthn(t *testing.T) { verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified) } -/* func TestServer_CreateSession_successfulIntent(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ @@ -354,7 +354,8 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) - intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id") + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + require.NoError(t, err) updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), Checks: &session.Checks{ @@ -369,9 +370,10 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { } func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() - intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id") + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + require.NoError(t, err) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ @@ -390,11 +392,12 @@ func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { } func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() // successful intent without known / linked user idpUserID := "id" - intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + require.NoError(t, err) // link the user (with info from intent) Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId()) @@ -418,7 +421,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { } func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -432,19 +435,18 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) - intentID := Instance.CreateIntent(t, CTX, idpID) + intent := Instance.CreateIntent(CTX, idpID) _, err = Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), Checks: &session.Checks{ IdpIntent: &session.CheckIDPIntent{ - IdpIntentId: intentID, + IdpIntentId: intent.GetIdpIntent().GetIdpIntentId(), IdpIntentToken: "false", }, }, }) require.Error(t, err) } -*/ func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) { resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ diff --git a/internal/api/grpc/settings/v2/settings.go b/internal/api/grpc/settings/v2/settings.go index 8b9ab9b845..77874bf970 100644 --- a/internal/api/grpc/settings/v2/settings.go +++ b/internal/api/grpc/settings/v2/settings.go @@ -23,6 +23,7 @@ func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSet Settings: loginSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.OrgID, }, @@ -38,6 +39,7 @@ func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *setting Settings: passwordComplexitySettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -53,6 +55,7 @@ func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.Ge Settings: passwordExpirySettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -68,6 +71,7 @@ func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrand Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -83,6 +87,7 @@ func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainS Settings: domainSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -98,6 +103,7 @@ func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.G Settings: legalAndSupportSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -113,6 +119,7 @@ func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockou Settings: lockoutSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, diff --git a/internal/api/grpc/settings/v2beta/settings.go b/internal/api/grpc/settings/v2beta/settings.go index 677d8f1c15..6193f129ba 100644 --- a/internal/api/grpc/settings/v2beta/settings.go +++ b/internal/api/grpc/settings/v2beta/settings.go @@ -23,6 +23,7 @@ func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSet Settings: loginSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.OrgID, }, @@ -38,6 +39,7 @@ func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *setting Settings: passwordComplexitySettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -53,6 +55,7 @@ func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.Ge Settings: passwordExpirySettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -68,6 +71,7 @@ func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrand Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -83,6 +87,7 @@ func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainS Settings: domainSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -98,6 +103,7 @@ func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.G Settings: legalAndSupportSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, @@ -113,6 +119,7 @@ func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockou Settings: lockoutSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go index 551079aec5..6826cb5694 100644 --- a/internal/api/grpc/system/instance_converter.go +++ b/internal/api/grpc/system/instance_converter.go @@ -15,6 +15,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" instance_pb "github.com/zitadel/zitadel/pkg/grpc/instance" + member_pb "github.com/zitadel/zitadel/pkg/grpc/member" system_pb "github.com/zitadel/zitadel/pkg/grpc/system" ) @@ -271,12 +272,29 @@ func ListIAMMembersRequestToQuery(req *system_pb.ListIAMMembersRequest) (*query. return &query.IAMMembersQuery{ MembersQuery: query.MembersQuery{ SearchRequest: query.SearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - // SortingColumn: model.IAMMemberSearchKey, //TOOD: not implemented in proto + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToMemberColumn(req.SortingColumn), }, Queries: queries, }, }, nil } + +func fieldNameToMemberColumn(fieldName member_pb.MemberFieldColumnName) query.Column { + switch fieldName { + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_UNSPECIFIED: + return query.InstanceMemberInstanceID + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_ID: + return query.InstanceMemberUserID + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CREATION_DATE: + return query.InstanceMemberCreationDate + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CHANGE_DATE: + return query.InstanceMemberChangeDate + case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_RESOURCE_OWNER: + return query.InstanceMemberResourceOwner + default: + return query.Column{} + } +} diff --git a/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go b/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go index 24c224b0fe..b705618f68 100644 --- a/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go +++ b/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go @@ -73,6 +73,7 @@ func requireEventually( assertCounts func(assert.TestingT, *eventCounts), msg string, ) (counts *eventCounts) { + t.Helper() countTimeout := 30 * time.Second assertTimeout := countTimeout + time.Second countCtx, cancel := context.WithTimeout(ctx, time.Minute) diff --git a/internal/api/grpc/system/integration_test/quotas_enabled/quota_test.go b/internal/api/grpc/system/integration_test/quotas_enabled/quota_test.go index 09c42eeb97..e11169421d 100644 --- a/internal/api/grpc/system/integration_test/quotas_enabled/quota_test.go +++ b/internal/api/grpc/system/integration_test/quotas_enabled/quota_test.go @@ -31,27 +31,10 @@ func TestServer_QuotaNotification_Limit(t *testing.T) { percent := 50 percentAmount := amount * percent / 100 - _, err := integration.SystemClient().SetQuota(CTX, &system.SetQuotaRequest{ - InstanceId: instance.Instance.Id, - Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED, - From: timestamppb.Now(), - ResetInterval: durationpb.New(time.Minute * 5), - Amount: uint64(amount), - Limit: true, - Notifications: []*quota_pb.Notification{ - { - Percent: uint32(percent), - Repeat: true, - CallUrl: callURL, - }, - { - Percent: 100, - Repeat: true, - CallUrl: callURL, - }, - }, + setQuota(t, instance.Instance.Id, amount, true, []*quota_pb.Notification{ + {Percent: uint32(percent), Repeat: true, CallUrl: callURL}, + {Percent: 100, Repeat: true, CallUrl: callURL}, }) - require.NoError(t, err) sub := sink.Subscribe(CTX, sink.ChannelQuota) defer sub.Close() @@ -72,6 +55,22 @@ func TestServer_QuotaNotification_Limit(t *testing.T) { require.Error(t, limitErr) } +func setQuota(t *testing.T, instanceID string, amount int, limit bool, notifications []*quota_pb.Notification) { + _, err := integration.SystemClient().SetQuota(CTX, &system.SetQuotaRequest{ + InstanceId: instanceID, + Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED, + From: timestamppb.Now(), + ResetInterval: durationpb.New(time.Minute * 5), + Amount: uint64(amount), + Limit: limit, + Notifications: notifications, + }) + require.NoError(t, err) + + // wait for some time as there is an eventual consistency until the quota is applied and used in the interceptor + time.Sleep(time.Second * 5) +} + func TestServer_QuotaNotification_NoLimit(t *testing.T) { instance := integration.NewInstance(CTX) iamCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -80,27 +79,10 @@ func TestServer_QuotaNotification_NoLimit(t *testing.T) { percent := 50 percentAmount := amount * percent / 100 - _, err := integration.SystemClient().SetQuota(CTX, &system.SetQuotaRequest{ - InstanceId: instance.Instance.Id, - Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED, - From: timestamppb.Now(), - ResetInterval: durationpb.New(time.Minute * 5), - Amount: uint64(amount), - Limit: false, - Notifications: []*quota_pb.Notification{ - { - Percent: uint32(percent), - Repeat: false, - CallUrl: callURL, - }, - { - Percent: 100, - Repeat: true, - CallUrl: callURL, - }, - }, + setQuota(t, instance.Instance.Id, amount, false, []*quota_pb.Notification{ + {Percent: uint32(percent), Repeat: false, CallUrl: callURL}, + {Percent: 100, Repeat: true, CallUrl: callURL}, }) - require.NoError(t, err) sub := sink.Subscribe(CTX, sink.ChannelQuota) defer sub.Close() diff --git a/internal/api/grpc/user/v2/integration_test/query_test.go b/internal/api/grpc/user/v2/integration_test/query_test.go index 2551a4a833..15dc959151 100644 --- a/internal/api/grpc/user/v2/integration_test/query_test.go +++ b/internal/api/grpc/user/v2/integration_test/query_test.go @@ -4,6 +4,7 @@ package user_test import ( "context" + "errors" "fmt" "slices" "testing" @@ -16,10 +17,61 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) +var ( + permissionCheckV2SetFlagInital bool + permissionCheckV2SetFlag bool +) + +type permissionCheckV2SettingsStruct struct { + TestNamePrependString string + SetFlag bool +} + +var permissionCheckV2Settings []permissionCheckV2SettingsStruct = []permissionCheckV2SettingsStruct{ + { + SetFlag: false, + TestNamePrependString: "permission_check_v2 IS NOT SET" + " ", + }, + { + SetFlag: true, + TestNamePrependString: "permission_check_v2 IS SET" + " ", + }, +} + +func setPermissionCheckV2Flag(t *testing.T, setFlag bool) { + if permissionCheckV2SetFlagInital && permissionCheckV2SetFlag == setFlag { + return + } + + _, err := Instance.Client.FeatureV2.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: &setFlag, + }) + require.NoError(t, err) + + var flagSet bool + for i := 0; !flagSet || i < 6; i++ { + res, err := Instance.Client.FeatureV2.GetInstanceFeatures(IamCTX, &feature.GetInstanceFeaturesRequest{}) + require.NoError(t, err) + if res.PermissionCheckV2.Enabled == setFlag { + flagSet = true + continue + } + time.Sleep(10 * time.Second) + } + + if !flagSet { + require.NoError(t, errors.New("unable to set permission_check_v2 flag")) + } + permissionCheckV2SetFlagInital = true + permissionCheckV2SetFlag = setFlag +} + func TestServer_GetUserByID(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { @@ -98,6 +150,7 @@ func TestServer_GetUserByID(t *testing.T) { }, Details: &object.Details{ ChangeDate: timestamppb.Now(), + CreationDate: timestamppb.Now(), ResourceOwner: orgResp.OrganizationId, }, }, @@ -143,6 +196,7 @@ func TestServer_GetUserByID(t *testing.T) { }, Details: &object.Details{ ChangeDate: timestamppb.Now(), + CreationDate: timestamppb.Now(), ResourceOwner: orgResp.OrganizationId, }, }, @@ -230,6 +284,7 @@ func TestServer_GetUserByID_Permission(t *testing.T) { }, Details: &object.Details{ ChangeDate: timestamppb.Now(), + CreationDate: timestamppb.Now(), ResourceOwner: newOrg.GetOrganizationId(), }, }, @@ -268,6 +323,7 @@ func TestServer_GetUserByID_Permission(t *testing.T) { }, Details: &object.Details{ ChangeDate: timestamppb.Now(), + CreationDate: timestamppb.Now(), ResourceOwner: newOrg.GetOrganizationId(), }, }, @@ -359,10 +415,16 @@ func createUsers(ctx context.Context, orgID string, count int, passwordChangeReq func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr { username := gofakeit.Email() + return createUserWithUserName(ctx, username, orgID, passwordChangeRequired) +} + +func createUserWithUserName(ctx context.Context, username string, orgID string, passwordChangeRequired bool) userAttr { // used as default country prefix phone := "+41" + gofakeit.Phone() resp := Instance.CreateHumanUserVerified(ctx, orgID, username, phone) info := userAttr{resp.GetUserId(), username, phone, nil, resp.GetDetails()} + // as the change date of the creation is the creation date + resp.Details.CreationDate = resp.GetDetails().GetChangeDate() if passwordChangeRequired { details := Instance.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true) info.Changed = details.GetChangeDate() @@ -371,6 +433,11 @@ func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) } func TestServer_ListUsers(t *testing.T) { + defer func() { + _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { ctx context.Context @@ -384,7 +451,7 @@ func TestServer_ListUsers(t *testing.T) { wantErr bool }{ { - name: "list user by id, no permission", + name: "list user by id, no permission machine user", args: args{ UserCTX, &user.ListUsersRequest{}, @@ -403,17 +470,77 @@ func TestServer_ListUsers(t *testing.T) { Result: []*user.User{}, }, }, + { + name: "list user by id, no permission human user", + args: func() args { + info := createUser(IamCTX, orgResp.OrganizationId, true) + // create session to get token + userID := info.UserID + createResp, err := Instance.Client.SessionV2.CreateSession(IamCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{UserId: userID}, + }, + Password: &session.CheckPassword{ + Password: integration.UserPassword, + }, + }, + }) + if err != nil { + require.NoError(t, err) + } + // use token to get ctx + HumanCTX := integration.WithAuthorizationToken(IamCTX, createResp.GetSessionToken()) + return args{ + HumanCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{info} + }, + } + }(), + want: &user.ListUsersResponse{ // human user should return itself when calling ListUsers() even if it has no permissions + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + PasswordChangeRequired: true, + PasswordChanged: timestamppb.Now(), + }, + }, + }, + }, + }, + }, { name: "list user by id, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, @@ -453,13 +580,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id, passwordChangeRequired, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, true) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, @@ -501,13 +626,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id multiple, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs())) return infos }, @@ -560,7 +683,8 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, - }, { + }, + { State: user.UserState_USER_STATE_ACTIVE, Type: &user.User_Human{ Human: &user.HumanUser{ @@ -588,13 +712,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by username, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, UsernameQuery(info.Username)) return []userAttr{info} }, @@ -634,13 +756,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username})) return []userAttr{info} }, @@ -678,189 +798,12 @@ func TestServer_ListUsers(t *testing.T) { }, { name: "list user in emails multiple, ok", - args: args{ - IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - infos := createUsers(ctx, orgResp.OrganizationId, 3, false) - request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) - return infos - }, - }, - want: &user.ListUsersResponse{ - Details: &object.ListDetails{ - TotalResult: 3, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*user.User{ - { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: "Mickey", - FamilyName: "Mouse", - NickName: gu.Ptr("Mickey"), - DisplayName: gu.Ptr("Mickey Mouse"), - PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), - }, - Email: &user.HumanEmail{ - IsVerified: true, - }, - Phone: &user.HumanPhone{ - IsVerified: true, - }, - }, - }, - }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: "Mickey", - FamilyName: "Mouse", - NickName: gu.Ptr("Mickey"), - DisplayName: gu.Ptr("Mickey Mouse"), - PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), - }, - Email: &user.HumanEmail{ - IsVerified: true, - }, - Phone: &user.HumanPhone{ - IsVerified: true, - }, - }, - }, - }, { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: "Mickey", - FamilyName: "Mouse", - NickName: gu.Ptr("Mickey"), - DisplayName: gu.Ptr("Mickey Mouse"), - PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), - }, - Email: &user.HumanEmail{ - IsVerified: true, - }, - Phone: &user.HumanPhone{ - IsVerified: true, - }, - }, - }, - }, - }, - }, - }, - { - name: "list user in emails no found, ok", - args: args{ - IamCTX, - &user.ListUsersRequest{Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - InUserEmailsQuery([]string{"notfound"}), - }, - }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - return []userAttr{} - }, - }, - want: &user.ListUsersResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*user.User{}, - }, - }, - { - name: "list user phone, ok", - args: args{ - IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - info := createUser(ctx, orgResp.OrganizationId, false) - request.Queries = append(request.Queries, PhoneQuery(info.Phone)) - return []userAttr{info} - }, - }, - want: &user.ListUsersResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*user.User{ - { - State: user.UserState_USER_STATE_ACTIVE, - Type: &user.User_Human{ - Human: &user.HumanUser{ - Profile: &user.HumanProfile{ - GivenName: "Mickey", - FamilyName: "Mouse", - NickName: gu.Ptr("Mickey"), - DisplayName: gu.Ptr("Mickey Mouse"), - PreferredLanguage: gu.Ptr("nl"), - Gender: user.Gender_GENDER_MALE.Enum(), - }, - Email: &user.HumanEmail{ - IsVerified: true, - }, - Phone: &user.HumanPhone{ - IsVerified: true, - }, - }, - }, - }, - }, - }, - }, - { - name: "list user in emails no found, ok", - args: args{ - IamCTX, - &user.ListUsersRequest{Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - InUserEmailsQuery([]string{"notfound"}), - }, - }, - func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - return []userAttr{} - }, - }, - want: &user.ListUsersResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*user.User{}, - }, - }, - { - name: "list user resourceowner multiple, ok", args: args{ IamCTX, &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { - orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email()) - infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) return infos @@ -937,93 +880,446 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, + { + name: "list user in emails no found, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, + }, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{}, + }, + }, + { + name: "list user phone, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) + request.Queries = append(request.Queries, PhoneQuery(info.Phone)) + return []userAttr{info} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user in emails no found, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, + }, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{}, + }, + }, + { + name: "list user resourceowner multiple, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email()) + + infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) + request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) + return infos + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 3, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user with org query", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + info := createUser(ctx, orgRespForOrgTests.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId)) + return []userAttr{info, {}} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 2, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, + // this is the admin of the org craated in Instance.CreateOrganization() + nil, + }, + }, + }, + { + name: "list user with wrong org query", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + // info := createUser(ctx, orgRespForOrgTests.OrganizationId, false) + createUser(ctx, orgRespForOrgTests.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests2.OrganizationId)) + return []userAttr{{}} + }, + }, + want: &user.ListUsersResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + // this is the admin of the org craated in Instance.CreateOrganization() + nil, + }, + }, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - infos := tt.args.dep(IamCTX, tt.args.req) + for _, f := range permissionCheckV2Settings { + f := f + for _, tt := range tests { + t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) { + setPermissionCheckV2Flag(t, f.SetFlag) + infos := tt.args.dep(IamCTX, tt.args.req) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.ListUsers(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - require.NoError(ttt, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListUsers(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + require.NoError(ttt, err) - // always only give back dependency infos which are required for the response - require.Len(ttt, tt.want.Result, len(infos)) - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions - tt.want.Details.TotalResult = got.Details.TotalResult + // always only give back dependency infos which are required for the response + require.Len(ttt, tt.want.Result, len(infos)) + if assert.Len(ttt, got.Result, len(tt.want.Result)) { + tt.want.Details.TotalResult = got.Details.TotalResult - // fill in userid and username as it is generated - for i := range infos { - tt.want.Result[i].UserId = infos[i].UserID - tt.want.Result[i].Username = infos[i].Username - tt.want.Result[i].PreferredLoginName = infos[i].Username - tt.want.Result[i].LoginNames = []string{infos[i].Username} - if human := tt.want.Result[i].GetHuman(); human != nil { - human.Email.Email = infos[i].Username - human.Phone.Phone = infos[i].Phone - if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { - human.PasswordChanged = infos[i].Changed + // fill in userid and username as it is generated + for i := range infos { + if tt.want.Result[i] == nil { + continue + } + tt.want.Result[i].UserId = infos[i].UserID + tt.want.Result[i].Username = infos[i].Username + tt.want.Result[i].PreferredLoginName = infos[i].Username + tt.want.Result[i].LoginNames = []string{infos[i].Username} + if human := tt.want.Result[i].GetHuman(); human != nil { + human.Email.Email = infos[i].Username + human.Phone.Phone = infos[i].Phone + if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { + human.PasswordChanged = infos[i].Changed + } + } + tt.want.Result[i].Details = infos[i].Details + } + for i := range tt.want.Result { + if tt.want.Result[i] == nil { + continue + } + assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) + } + } + integration.AssertListDetails(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected user result") + }) + } + } +} + +func TestServer_SystemUsers_ListUsers(t *testing.T) { + defer func() { + _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }() + + org1 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) + org2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), "org2@zitadel.com") + org3 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser1@zitadel.com", org1.OrganizationId, false) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser2@zitadel.com", org2.OrganizationId, false) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser3@zitadel.com", org3.OrganizationId, false) + + tests := []struct { + name string + ctx context.Context + req *user.ListUsersRequest + expectedFoundUsernames []string + checkNumberOfUsersReturned bool + }{ + { + name: "list users with neccessary permissions", + ctx: SystemCTX, + req: &user.ListUsersRequest{}, + // the number of users returned will vary from test run to test run, + // so just check the system user gets back users from different orgs whcih it is not a memeber of + checkNumberOfUsersReturned: false, + expectedFoundUsernames: []string{"Test_SystemUsers_ListUser1@zitadel.com", "Test_SystemUsers_ListUser2@zitadel.com", "Test_SystemUsers_ListUser3@zitadel.com"}, + }, + { + name: "list users without neccessary permissions", + ctx: SystemUserWithNoPermissionsCTX, + req: &user.ListUsersRequest{}, + // check no users returned + checkNumberOfUsersReturned: true, + }, + { + name: "list users with neccessary permissions specifying org", + req: &user.ListUsersRequest{ + Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, + }, + ctx: SystemCTX, + expectedFoundUsernames: []string{"Test_SystemUsers_ListUser2@zitadel.com", "org2@zitadel.com"}, + checkNumberOfUsersReturned: true, + }, + { + name: "list users without neccessary permissions specifying org", + req: &user.ListUsersRequest{ + Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, + }, + ctx: SystemUserWithNoPermissionsCTX, + // check no users returned + checkNumberOfUsersReturned: true, + }, + } + + for _, f := range permissionCheckV2Settings { + f := f + for _, tt := range tests { + t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) { + setPermissionCheckV2Flag(t, f.SetFlag) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, 1*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListUsers(tt.ctx, tt.req) + require.NoError(ttt, err) + + if tt.checkNumberOfUsersReturned { + require.Equal(t, len(tt.expectedFoundUsernames), len(got.Result)) + } + + if tt.expectedFoundUsernames != nil { + for _, user := range got.Result { + for i, username := range tt.expectedFoundUsernames { + if username == user.Username { + tt.expectedFoundUsernames = tt.expectedFoundUsernames[i+1:] + break + } + } + if len(tt.expectedFoundUsernames) == 0 { + return } } - tt.want.Result[i].Details = infos[i].Details + require.FailNow(t, "unable to find all users with specified usernames") } - for i := range tt.want.Result { - assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) - } - } - integration.AssertListDetails(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected user result") - }) + }, retryDuration, tick, "timeout waiting for expected user result") + }) + } } } func InUserIDsQuery(ids []string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_InUserIdsQuery{ - InUserIdsQuery: &user.InUserIDQuery{ - UserIds: ids, + return &user.SearchQuery{ + Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{ + UserIds: ids, + }, }, - }, } } func InUserEmailsQuery(emails []string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_InUserEmailsQuery{ - InUserEmailsQuery: &user.InUserEmailsQuery{ - UserEmails: emails, + return &user.SearchQuery{ + Query: &user.SearchQuery_InUserEmailsQuery{ + InUserEmailsQuery: &user.InUserEmailsQuery{ + UserEmails: emails, + }, }, - }, } } func PhoneQuery(number string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_PhoneQuery{ - PhoneQuery: &user.PhoneQuery{ - Number: number, + return &user.SearchQuery{ + Query: &user.SearchQuery_PhoneQuery{ + PhoneQuery: &user.PhoneQuery{ + Number: number, + }, }, - }, } } func UsernameQuery(username string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{ - UserNameQuery: &user.UserNameQuery{ - UserName: username, + return &user.SearchQuery{ + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: username, + }, }, - }, } } func OrganizationIdQuery(resourceowner string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_OrganizationIdQuery{ - OrganizationIdQuery: &user.OrganizationIdQuery{ - OrganizationId: resourceowner, + return &user.SearchQuery{ + Query: &user.SearchQuery_OrganizationIdQuery{ + OrganizationIdQuery: &user.OrganizationIdQuery{ + OrganizationId: resourceowner, + }, }, - }, } } diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 0ed93fd92e..bf396fd25d 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -17,9 +17,11 @@ import ( "github.com/zitadel/logging" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/pkg/grpc/auth" "github.com/zitadel/zitadel/pkg/grpc/idp" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" @@ -29,12 +31,13 @@ import ( ) var ( - CTX context.Context - IamCTX context.Context - UserCTX context.Context - SystemCTX context.Context - Instance *integration.Instance - Client user.UserServiceClient + CTX context.Context + IamCTX context.Context + UserCTX context.Context + SystemCTX context.Context + SystemUserWithNoPermissionsCTX context.Context + Instance *integration.Instance + Client user.UserServiceClient ) func TestMain(m *testing.M) { @@ -44,6 +47,7 @@ func TestMain(m *testing.M) { Instance = integration.NewInstance(ctx) + SystemUserWithNoPermissionsCTX = integration.WithSystemUserWithNoPermissionsAuthorization(ctx) UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission) IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) SystemCTX = integration.WithSystemAuthorization(ctx) @@ -1304,7 +1308,6 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) @@ -2110,15 +2113,31 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } } -/* func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) - intentID := Instance.CreateIntent(t, CTX, idpID) - successfulID, token, changeDate, sequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", "id") - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "user", "id") - ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Instance.CreateSuccessfulLDAPIntent(t, CTX, idpID, "", "id") - ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Instance.CreateSuccessfulLDAPIntent(t, CTX, idpID, "user", "id") - samlSuccessfulID, samlToken, samlChangeDate, samlSequence := Instance.CreateSuccessfulSAMLIntent(t, CTX, idpID, "", "id") + oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() + oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() + samlIdpID := Instance.AddSAMLPostProvider(IamCTX) + ldapIdpID := Instance.AddLDAPProvider(IamCTX) + authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) + require.NoError(t, err) + intentID := authURL.Query().Get("state") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "") + require.NoError(t, err) + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user") + require.NoError(t, err) + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") + require.NoError(t, err) + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + require.NoError(t, err) + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") + require.NoError(t, err) + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") + require.NoError(t, err) + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "") + require.NoError(t, err) + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user") + require.NoError(t, err) type args struct { ctx context.Context req *user.RetrieveIdentityProviderIntentRequest @@ -2152,7 +2171,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { wantErr: true, }, { - name: "retrieve successful intent", + name: "retrieve successful oauth intent", args: args{ CTX, &user.RetrieveIdentityProviderIntentRequest{ @@ -2173,18 +2192,31 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, UserId: "id", - UserName: "username", + UserName: "", RawInformation: func() *structpb.Struct { s, err := structpb.NewStruct(map[string]interface{}{ - "sub": "id", - "preferred_username": "username", + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, }) require.NoError(t, err) return s }(), }, + AddHumanUser: &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: oauthIdpID, UserId: "id"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, }, wantErr: false, }, @@ -2211,7 +2243,97 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessful, + IdpIntentToken: oidcToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcSequence, + }, + UserId: "", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + AddHumanUser: &user.AddHumanUserRequest{ + Username: gu.Ptr("username"), + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: oidcIdpID, UserId: "id", UserName: "username"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessfulWithUserID, + IdpIntentToken: oidcWithUserIDToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcWithUserIDChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcWithUserIDSequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2255,7 +2377,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2268,6 +2390,18 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { return s }(), }, + AddHumanUser: &user.AddHumanUserRequest{ + Username: gu.Ptr("username"), + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("en"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: ldapIdpID, UserId: "id", UserName: "username"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, }, wantErr: false, }, @@ -2301,7 +2435,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2338,7 +2472,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { Assertion: []byte(""), }, }, - IdpId: idpID, + IdpId: samlIdpID, UserId: "id", UserName: "", RawInformation: func() *structpb.Struct { @@ -2352,6 +2486,56 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { return s }(), }, + AddHumanUser: &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: samlIdpID, UserId: "id"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, + }, + wantErr: false, + }, + { + name: "retrieve successful saml intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: samlSuccessfulWithUserID, + IdpIntentToken: samlWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(samlWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: samlWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: []byte(""), + }, + }, + IdpId: samlIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "attributes": map[string]interface{}{ + "attribute1": []interface{}{"value1"}, + }, + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", }, wantErr: false, }, @@ -2361,15 +2545,14 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { got, err := Client.RetrieveIdentityProviderIntent(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) - } else { - require.NoError(t, err) + return } + require.NoError(t, err) - grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + assert.EqualExportedValues(t, tt.want, got) }) } } -*/ func ctxFromNewUserWithRegisteredPasswordlessLegacy(t *testing.T) (context.Context, string, *auth.AddMyPasswordlessResponse) { userID := Instance.CreateHumanUser(CTX).GetUserId() @@ -2866,7 +3049,6 @@ func TestServer_ListAuthenticationFactors(t *testing.T) { assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult()) }, retryDuration, tick, "timeout waiting for expected auth methods result") - }) } } diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go new file mode 100644 index 0000000000..6e46dfd5c3 --- /dev/null +++ b/internal/api/grpc/user/v2/intent.go @@ -0,0 +1,370 @@ +package user + +import ( + "context" + "encoding/json" + "errors" + + oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/idp" + "github.com/zitadel/zitadel/internal/idp/providers/apple" + "github.com/zitadel/zitadel/internal/idp/providers/azuread" + "github.com/zitadel/zitadel/internal/idp/providers/github" + "github.com/zitadel/zitadel/internal/idp/providers/gitlab" + "github.com/zitadel/zitadel/internal/idp/providers/google" + "github.com/zitadel/zitadel/internal/idp/providers/jwt" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" + "github.com/zitadel/zitadel/internal/idp/providers/oauth" + "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) { + switch t := req.GetContent().(type) { + case *user.StartIdentityProviderIntentRequest_Urls: + return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls) + case *user.StartIdentityProviderIntentRequest_Ldap: + return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap) + default: + return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t) + } +} + +func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { + state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) + if err != nil { + return nil, err + } + _, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters()) + if err != nil { + return nil, err + } + content, redirect := session.GetAuth(ctx) + if redirect { + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, + }, nil + } + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ + PostForm: []byte(content), + }, + }, nil +} + +func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { + intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil) + if err != nil { + return nil, err + } + externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + if err != nil { + if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { + return nil, err + } + return nil, err + } + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes) + if err != nil { + return nil, err + } + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{ + IdpIntent: &user.IDPIntent{ + IdpIntentId: intentWriteModel.AggregateID, + IdpIntentToken: token, + UserId: userID, + }, + }, + }, nil +} + +func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) { + idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID) + if err != nil { + return "", err + } + externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID) + if err != nil { + return "", err + } + queries := []query.SearchQuery{ + idQuery, externalIDQuery, + } + links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) + if err != nil { + return "", err + } + if len(links.Links) == 1 { + return links.Links[0].UserID, nil + } + return "", nil +} + +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { + provider, err := s.command.GetProvider(ctx, idpID, "", "") + if err != nil { + return nil, "", nil, err + } + ldapProvider, ok := provider.(*ldap.Provider) + if !ok { + return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented") + } + session := ldapProvider.GetSession(username, password) + externalUser, err := session.FetchUser(ctx) + if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) { + return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed") + } + if err != nil { + return nil, "", nil, err + } + userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID()) + if err != nil { + return nil, "", nil, err + } + + attributes := make(map[string][]string, 0) + for _, item := range session.Entry.Attributes { + attributes[item.Name] = item.Values + } + return externalUser, userID, attributes, nil +} + +func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { + intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "") + if err != nil { + return nil, err + } + if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil { + return nil, err + } + if intent.State != domain.IDPIntentStateSucceeded { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") + } + idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg) + if err != nil { + return nil, err + } + if idpIntent.UserId == "" { + provider, err := s.command.GetProvider(ctx, idpIntent.IdpInformation.IdpId, "", "") + if err != nil && !errors.Is(err, oidc_pkg.ErrDiscoveryFailed) { + return nil, err + } + var idpUser idp.User + switch p := provider.(type) { + case *apple.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &apple.User{}) + case *oauth.Provider: + idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) + case *oidc.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + case *jwt.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &jwt.User{}) + case *azuread.Provider: + idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) + case *github.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{}) + case *gitlab.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + case *google.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + case *saml.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{}) + case *ldap.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &ldap.User{}) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "IDP-7rPBbls4Zn", "Errors.ExternalIDP.IDPTypeNotImplemented") + } + if err != nil { + return nil, err + } + idpIntent.AddHumanUser = idpUserToAddHumanUser(idpUser, idpIntent.IdpInformation.IdpId) + } + return idpIntent, nil +} + +type rawUserMapper struct { + RawInfo map[string]interface{} +} + +func unmarshalRawIdpUser(idpUserData []byte, idpUser idp.User) (idp.User, error) { + userMapper := &rawUserMapper{} + if err := json.Unmarshal(idpUserData, userMapper); err != nil { + return nil, err + } + idpUserData, err := json.Marshal(userMapper.RawInfo) + if err != nil { + return nil, err + } + return unmarshalIdpUser(idpUserData, idpUser) +} + +func unmarshalIdpUser(idpUserData []byte, idpUser idp.User) (idp.User, error) { + if err := json.Unmarshal(idpUserData, idpUser); err != nil { + return nil, err + } + return idpUser, nil +} + +func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { + rawInformation := new(structpb.Struct) + err = rawInformation.UnmarshalJSON(intent.IDPUser) + if err != nil { + return nil, err + } + information := &user.RetrieveIdentityProviderIntentResponse{ + IdpInformation: &user.IDPInformation{ + IdpId: intent.IDPID, + UserId: intent.IDPUserID, + UserName: intent.IDPUserName, + RawInformation: rawInformation, + }, + UserId: intent.UserID, + } + information.Details = intentToDetailsPb(intent) + // OAuth / OIDC + if intent.IDPIDToken != "" || intent.IDPAccessToken != nil { + information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg) + if err != nil { + return nil, err + } + } + // LDAP + if intent.IDPEntryAttributes != nil { + access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes) + if err != nil { + return nil, err + } + information.IdpInformation.Access = access + } + // SAML + if intent.Assertion != nil { + assertion, err := crypto.Decrypt(intent.Assertion, alg) + if err != nil { + return nil, err + } + information.IdpInformation.Access = IDPSAMLResponseToPb(assertion) + } + return information, nil +} + +func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) { + var idToken *string + if idpIDToken != "" { + idToken = &idpIDToken + } + var accessToken string + if idpAccessToken != nil { + accessToken, err = crypto.DecryptString(idpAccessToken, alg) + if err != nil { + return nil, err + } + } + return &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: accessToken, + IdToken: idToken, + }, + }, nil +} + +func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details { + return &object_pb.Details{ + Sequence: intent.ProcessedSequence, + ChangeDate: timestamppb.New(intent.ChangeDate), + ResourceOwner: intent.ResourceOwner, + } +} + +func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) { + values := make(map[string]interface{}, 0) + for k, v := range entryAttributes { + intValues := make([]interface{}, len(v)) + for i, value := range v { + intValues[i] = value + } + values[k] = intValues + } + attributes, err := structpb.NewStruct(values) + if err != nil { + return nil, err + } + return &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: attributes, + }, + }, nil +} + +func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml { + return &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: assertion, + }, + } +} + +func (s *Server) checkIntentToken(token string, intentID string) error { + return crypto.CheckToken(s.idpAlg, token, intentID) +} + +func idpUserToAddHumanUser(idpUser idp.User, idpID string) *user.AddHumanUserRequest { + addHumanUser := &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + GivenName: idpUser.GetFirstName(), + FamilyName: idpUser.GetLastName(), + }, + Email: &user.SetHumanEmail{ + Email: string(idpUser.GetEmail()), + Verification: &user.SetHumanEmail_SendCode{}, + }, + Metadata: make([]*user.SetMetadataEntry, 0), + IdpLinks: []*user.IDPLink{ + { + IdpId: idpID, + UserId: idpUser.GetID(), + UserName: idpUser.GetPreferredUsername(), + }, + }, + } + if username := idpUser.GetPreferredUsername(); username != "" { + addHumanUser.Username = &username + } + if nickName := idpUser.GetNickname(); nickName != "" { + addHumanUser.Profile.NickName = &nickName + } + if displayName := idpUser.GetDisplayName(); displayName != "" { + addHumanUser.Profile.DisplayName = &displayName + } + if lang := idpUser.GetPreferredLanguage().String(); lang != "" { + addHumanUser.Profile.PreferredLanguage = &lang + } + if isEmailVerified := idpUser.IsEmailVerified(); isEmailVerified { + addHumanUser.Email.Verification = &user.SetHumanEmail_IsVerified{IsVerified: isEmailVerified} + } + if phone := idpUser.GetPhone(); phone != "" { + addHumanUser.Phone = &user.SetHumanPhone{ + Phone: string(phone), + Verification: &user.SetHumanPhone_SendCode{}, + } + if isPhoneVerified := idpUser.IsPhoneVerified(); isPhoneVerified { + addHumanUser.Phone.Verification = &user.SetHumanPhone_IsVerified{IsVerified: isPhoneVerified} + } + } + return addHumanUser +} diff --git a/internal/api/grpc/user/v2/passkey_test.go b/internal/api/grpc/user/v2/passkey_test.go index 7facfc74e0..9263012b98 100644 --- a/internal/api/grpc/user/v2/passkey_test.go +++ b/internal/api/grpc/user/v2/passkey_test.go @@ -74,6 +74,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, ID: "123", @@ -90,6 +91,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, ID: "123", @@ -104,6 +106,10 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { Seconds: 3000, Nanos: 22, }, + CreationDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, ResourceOwner: "me", }, PasskeyId: "123", @@ -150,6 +156,7 @@ func Test_passkeyDetailsToPb(t *testing.T) { details: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, err: nil, @@ -161,6 +168,10 @@ func Test_passkeyDetailsToPb(t *testing.T) { Seconds: 3000, Nanos: 22, }, + CreationDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, ResourceOwner: "me", }, }, @@ -199,6 +210,7 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, CodeID: "123", @@ -213,6 +225,10 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) { Seconds: 3000, Nanos: 22, }, + CreationDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, ResourceOwner: "me", }, Code: &user.PasskeyRegistrationCode{ diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/query.go index aeb17d5dcf..136a4a0932 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -21,6 +21,7 @@ func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) return &user.GetUserByIDResponse{ Details: object.DomainToDetailsPb(&domain.ObjectDetails{ Sequence: resp.Sequence, + CreationDate: resp.CreationDate, EventDate: resp.ChangeDate, ResourceOwner: resp.ResourceOwner, }), @@ -58,6 +59,7 @@ func userToPb(userQ *query.User, assetPrefix string) *user.User { Sequence: userQ.Sequence, EventDate: userQ.ChangeDate, ResourceOwner: userQ.ResourceOwner, + CreationDate: userQ.CreationDate, }), State: userStateToPb(userQ.State), Username: userQ.Username, diff --git a/internal/api/grpc/user/v2/u2f_test.go b/internal/api/grpc/user/v2/u2f_test.go index 73366ab29b..fae3ba1cdb 100644 --- a/internal/api/grpc/user/v2/u2f_test.go +++ b/internal/api/grpc/user/v2/u2f_test.go @@ -43,6 +43,7 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, ID: "123", @@ -59,6 +60,7 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, ID: "123", @@ -73,6 +75,10 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) { Seconds: 3000, Nanos: 22, }, + CreationDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, ResourceOwner: "me", }, U2FId: "123", diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 9a092afacf..0f958f0d40 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -2,28 +2,19 @@ package user import ( "context" - "errors" "io" "golang.org/x/text/language" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/idp" - "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) { - human, err := AddUserRequestToAddHuman(req) if err != nil { return nil, err @@ -356,236 +347,6 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string { return converted } -func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) { - switch t := req.GetContent().(type) { - case *user.StartIdentityProviderIntentRequest_Urls: - return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls) - case *user.StartIdentityProviderIntentRequest_Ldap: - return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap) - default: - return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t) - } -} - -func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { - intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - content, redirect, err := s.command.AuthFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) - if err != nil { - return nil, err - } - if redirect { - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, - }, nil - } else { - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ - PostForm: []byte(content), - }, - }, nil - } -} - -func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { - intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) - if err != nil { - if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { - return nil, err - } - return nil, err - } - token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes) - if err != nil { - return nil, err - } - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{ - IdpIntent: &user.IDPIntent{ - IdpIntentId: intentWriteModel.AggregateID, - IdpIntentToken: token, - UserId: userID, - }, - }, - }, nil -} - -func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) { - idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID) - if err != nil { - return "", err - } - externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID) - if err != nil { - return "", err - } - queries := []query.SearchQuery{ - idQuery, externalIDQuery, - } - links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) - if err != nil { - return "", err - } - if len(links.Links) == 1 { - return links.Links[0].UserID, nil - } - return "", nil -} - -func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { - provider, err := s.command.GetProvider(ctx, idpID, "", "") - if err != nil { - return nil, "", nil, err - } - ldapProvider, ok := provider.(*ldap.Provider) - if !ok { - return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented") - } - session := ldapProvider.GetSession(username, password) - externalUser, err := session.FetchUser(ctx) - if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) { - return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed") - } - if err != nil { - return nil, "", nil, err - } - userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID()) - if err != nil { - return nil, "", nil, err - } - - attributes := make(map[string][]string, 0) - for _, item := range session.Entry.Attributes { - attributes[item.Name] = item.Values - } - return externalUser, userID, attributes, nil -} - -func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { - intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "") - if err != nil { - return nil, err - } - if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil { - return nil, err - } - if intent.State != domain.IDPIntentStateSucceeded { - return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") - } - return idpIntentToIDPIntentPb(intent, s.idpAlg) -} - -func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { - rawInformation := new(structpb.Struct) - err = rawInformation.UnmarshalJSON(intent.IDPUser) - if err != nil { - return nil, err - } - information := &user.RetrieveIdentityProviderIntentResponse{ - Details: intentToDetailsPb(intent), - IdpInformation: &user.IDPInformation{ - IdpId: intent.IDPID, - UserId: intent.IDPUserID, - UserName: intent.IDPUserName, - RawInformation: rawInformation, - }, - UserId: intent.UserID, - } - if intent.IDPIDToken != "" || intent.IDPAccessToken != nil { - information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg) - if err != nil { - return nil, err - } - } - - if intent.IDPEntryAttributes != nil { - access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes) - if err != nil { - return nil, err - } - information.IdpInformation.Access = access - } - - if intent.Assertion != nil { - assertion, err := crypto.Decrypt(intent.Assertion, alg) - if err != nil { - return nil, err - } - information.IdpInformation.Access = IDPSAMLResponseToPb(assertion) - } - - return information, nil -} - -func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) { - var idToken *string - if idpIDToken != "" { - idToken = &idpIDToken - } - var accessToken string - if idpAccessToken != nil { - accessToken, err = crypto.DecryptString(idpAccessToken, alg) - if err != nil { - return nil, err - } - } - return &user.IDPInformation_Oauth{ - Oauth: &user.IDPOAuthAccessInformation{ - AccessToken: accessToken, - IdToken: idToken, - }, - }, nil -} - -func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details { - return &object_pb.Details{ - Sequence: intent.ProcessedSequence, - ChangeDate: timestamppb.New(intent.ChangeDate), - ResourceOwner: intent.ResourceOwner, - } -} - -func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) { - values := make(map[string]interface{}, 0) - for k, v := range entryAttributes { - intValues := make([]interface{}, len(v)) - for i, value := range v { - intValues[i] = value - } - values[k] = intValues - } - attributes, err := structpb.NewStruct(values) - if err != nil { - return nil, err - } - return &user.IDPInformation_Ldap{ - Ldap: &user.IDPLDAPAccessInformation{ - Attributes: attributes, - }, - }, nil -} - -func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml { - return &user.IDPInformation_Saml{ - Saml: &user.IDPSAMLAccessInformation{ - Assertion: assertion, - }, - } -} - -func (s *Server) checkIntentToken(token string, intentID string) error { - return crypto.CheckToken(s.idpAlg, token, intentID) -} - func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) { authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true, req.GetDomainQuery().GetIncludeWithoutDomain(), req.GetDomainQuery().GetDomain()) if err != nil { diff --git a/internal/api/grpc/user/v2/user_test.go b/internal/api/grpc/user/v2/user_test.go index 9e7a5a5ab0..9408b3acf9 100644 --- a/internal/api/grpc/user/v2/user_test.go +++ b/internal/api/grpc/user/v2/user_test.go @@ -11,7 +11,6 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -322,7 +321,7 @@ func Test_idpIntentToIDPIntentPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := idpIntentToIDPIntentPb(tt.args.intent, tt.args.alg) require.ErrorIs(t, err, tt.res.err) - grpc.AllFieldsEqual(t, tt.res.resp.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + assert.EqualExportedValues(t, tt.res.resp, got) }) } } diff --git a/internal/api/grpc/user/v2beta/integration_test/query_test.go b/internal/api/grpc/user/v2beta/integration_test/query_test.go index 67fc609212..73bff3fd0d 100644 --- a/internal/api/grpc/user/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/query_test.go @@ -4,6 +4,7 @@ package user_test import ( "context" + "errors" "fmt" "slices" "testing" @@ -16,8 +17,10 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" "github.com/zitadel/zitadel/pkg/grpc/object/v2" object_v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) @@ -29,6 +32,55 @@ func detailsV2ToV2beta(obj *object.Details) *object_v2beta.Details { } } +var ( + permissionCheckV2SetFlagInital bool + permissionCheckV2SetFlag bool +) + +type permissionCheckV2SettingsStruct struct { + TestNamePrependString string + SetFlag bool +} + +var permissionCheckV2Settings []permissionCheckV2SettingsStruct = []permissionCheckV2SettingsStruct{ + { + SetFlag: false, + TestNamePrependString: "permission_check_v2 IS NOT SET" + " ", + }, + { + SetFlag: true, + TestNamePrependString: "permission_check_v2 IS SET" + " ", + }, +} + +func setPermissionCheckV2Flag(t *testing.T, setFlag bool) { + if permissionCheckV2SetFlagInital && permissionCheckV2SetFlag == setFlag { + return + } + + _, err := Instance.Client.FeatureV2.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: &setFlag, + }) + require.NoError(t, err) + + var flagSet bool + for i := 0; !flagSet || i < 6; i++ { + res, err := Instance.Client.FeatureV2.GetInstanceFeatures(IamCTX, &feature.GetInstanceFeaturesRequest{}) + require.NoError(t, err) + if res.PermissionCheckV2.Enabled == setFlag { + flagSet = true + continue + } + time.Sleep(10 * time.Second) + } + + if !flagSet { + require.NoError(t, errors.New("unable to set permission_check_v2 flag")) + } + permissionCheckV2SetFlagInital = true + permissionCheckV2SetFlag = setFlag +} + func TestServer_GetUserByID(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { @@ -381,6 +433,11 @@ func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) } func TestServer_ListUsers(t *testing.T) { + defer func() { + _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { ctx context.Context @@ -394,7 +451,7 @@ func TestServer_ListUsers(t *testing.T) { wantErr bool }{ { - name: "list user by id, no permission", + name: "list user by id, no permission machine user", args: args{ UserCTX, &user.ListUsersRequest{}, @@ -413,17 +470,77 @@ func TestServer_ListUsers(t *testing.T) { Result: []*user.User{}, }, }, + { + name: "list user by id, no permission human user", + args: func() args { + info := createUser(IamCTX, orgResp.OrganizationId, true) + // create session to get token + userID := info.UserID + createResp, err := Instance.Client.SessionV2.CreateSession(IamCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{UserId: userID}, + }, + Password: &session.CheckPassword{ + Password: integration.UserPassword, + }, + }, + }) + if err != nil { + require.NoError(t, err) + } + // use token to get ctx + HumanCTX := integration.WithAuthorizationToken(IamCTX, createResp.GetSessionToken()) + return args{ + HumanCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{info} + }, + } + }(), + want: &user.ListUsersResponse{ // human user should return itself when calling ListUsers() even if it has no permissions + Details: &object_v2beta.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + PasswordChangeRequired: true, + PasswordChanged: timestamppb.Now(), + }, + }, + }, + }, + }, + }, { name: "list user by id, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, @@ -463,13 +580,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id, passwordChangeRequired, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, true) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID})) return []userAttr{info} }, @@ -511,13 +626,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by id multiple, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs())) return infos }, @@ -549,7 +662,8 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, - }, { + }, + { State: user.UserState_USER_STATE_ACTIVE, Type: &user.User_Human{ Human: &user.HumanUser{ @@ -569,7 +683,8 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, - }, { + }, + { State: user.UserState_USER_STATE_ACTIVE, Type: &user.User_Human{ Human: &user.HumanUser{ @@ -597,13 +712,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user by username, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, UsernameQuery(info.Username)) return []userAttr{info} }, @@ -650,6 +763,7 @@ func TestServer_ListUsers(t *testing.T) { }, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username})) return []userAttr{info} }, @@ -689,13 +803,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails multiple, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) return infos }, @@ -775,10 +887,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user in emails no found, ok", args: args{ IamCTX, - &user.ListUsersRequest{Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - InUserEmailsQuery([]string{"notfound"}), - }, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, }, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { return []userAttr{} @@ -797,13 +910,11 @@ func TestServer_ListUsers(t *testing.T) { name: "list user phone, ok", args: args{ IamCTX, - &user.ListUsersRequest{ - Queries: []*user.SearchQuery{ - OrganizationIdQuery(orgResp.OrganizationId), - }, - }, + &user.ListUsersRequest{}, func(ctx context.Context, request *user.ListUsersRequest) userAttrs { info := createUser(ctx, orgResp.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, PhoneQuery(info.Phone)) return []userAttr{info} }, @@ -839,6 +950,29 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, + { + name: "list user in emails no found, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, + }, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{} + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{}, + }, + }, { name: "list user resourceowner multiple, ok", args: args{ @@ -848,6 +982,7 @@ func TestServer_ListUsers(t *testing.T) { orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email()) infos := createUsers(ctx, orgResp.OrganizationId, 3, false) + request.Queries = []*user.SearchQuery{} request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails())) return infos @@ -924,93 +1059,182 @@ func TestServer_ListUsers(t *testing.T) { }, }, }, + { + name: "list user with org query", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + info := createUser(ctx, orgRespForOrgTests.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId)) + return []userAttr{info, {}} + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 2, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + IsVerified: true, + }, + }, + }, + }, + // this is the admin of the org craated in Instance.CreateOrganization() + nil, + }, + }, + }, + { + name: "list user with wrong org query", + args: args{ + IamCTX, + &user.ListUsersRequest{}, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) + // info := createUser(ctx, orgRespForOrgTests.OrganizationId, false) + createUser(ctx, orgRespForOrgTests.OrganizationId, false) + request.Queries = []*user.SearchQuery{} + request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests2.OrganizationId)) + return []userAttr{{}} + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + // this is the admin of the org craated in Instance.CreateOrganization() + nil, + }, + }, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - infos := tt.args.dep(IamCTX, tt.args.req) + for _, f := range permissionCheckV2Settings { + f := f + for _, tt := range tests { + t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) { + setPermissionCheckV2Flag(t, f.SetFlag) + infos := tt.args.dep(IamCTX, tt.args.req) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.ListUsers(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - require.NoError(ttt, err) + // retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 10*time.Minute) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 20*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListUsers(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + require.NoError(ttt, err) - // always only give back dependency infos which are required for the response - require.Len(ttt, tt.want.Result, len(infos)) - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions - tt.want.Details.TotalResult = got.Details.TotalResult + // always only give back dependency infos which are required for the response + require.Len(ttt, tt.want.Result, len(infos)) + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Result, len(tt.want.Result)) { + // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions + tt.want.Details.TotalResult = got.Details.TotalResult - // fill in userid and username as it is generated - for i := range infos { - tt.want.Result[i].UserId = infos[i].UserID - tt.want.Result[i].Username = infos[i].Username - tt.want.Result[i].PreferredLoginName = infos[i].Username - tt.want.Result[i].LoginNames = []string{infos[i].Username} - if human := tt.want.Result[i].GetHuman(); human != nil { - human.Email.Email = infos[i].Username - human.Phone.Phone = infos[i].Phone - if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { - human.PasswordChanged = infos[i].Changed + // fill in userid and username as it is generated + for i := range infos { + if tt.want.Result[i] == nil { + continue } + tt.want.Result[i].UserId = infos[i].UserID + tt.want.Result[i].Username = infos[i].Username + tt.want.Result[i].PreferredLoginName = infos[i].Username + tt.want.Result[i].LoginNames = []string{infos[i].Username} + if human := tt.want.Result[i].GetHuman(); human != nil { + human.Email.Email = infos[i].Username + human.Phone.Phone = infos[i].Phone + if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { + human.PasswordChanged = infos[i].Changed + } + } + tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details) + } + for i := range tt.want.Result { + if tt.want.Result[i] == nil { + continue + } + assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) } - tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details) } - for i := range tt.want.Result { - assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i]) - } - } - integration.AssertListDetails(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected user result") - }) + integration.AssertListDetails(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected user result") + }) + } } } func InUserIDsQuery(ids []string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_InUserIdsQuery{ - InUserIdsQuery: &user.InUserIDQuery{ - UserIds: ids, + return &user.SearchQuery{ + Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{ + UserIds: ids, + }, }, - }, } } func InUserEmailsQuery(emails []string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_InUserEmailsQuery{ - InUserEmailsQuery: &user.InUserEmailsQuery{ - UserEmails: emails, + return &user.SearchQuery{ + Query: &user.SearchQuery_InUserEmailsQuery{ + InUserEmailsQuery: &user.InUserEmailsQuery{ + UserEmails: emails, + }, }, - }, } } func PhoneQuery(number string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_PhoneQuery{ - PhoneQuery: &user.PhoneQuery{ - Number: number, + return &user.SearchQuery{ + Query: &user.SearchQuery_PhoneQuery{ + PhoneQuery: &user.PhoneQuery{ + Number: number, + }, }, - }, } } func UsernameQuery(username string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{ - UserNameQuery: &user.UserNameQuery{ - UserName: username, + return &user.SearchQuery{ + Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: username, + }, }, - }, } } func OrganizationIdQuery(resourceowner string) *user.SearchQuery { - return &user.SearchQuery{Query: &user.SearchQuery_OrganizationIdQuery{ - OrganizationIdQuery: &user.OrganizationIdQuery{ - OrganizationId: resourceowner, + return &user.SearchQuery{ + Query: &user.SearchQuery_OrganizationIdQuery{ + OrganizationIdQuery: &user.OrganizationIdQuery{ + OrganizationId: resourceowner, + }, }, - }, } } diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index 9cf59ae563..a81de58761 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -16,9 +16,12 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/pkg/grpc/idp" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" @@ -2142,15 +2145,31 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } } -/* func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(t, CTX) - intentID := Instance.CreateIntent(t, CTX, idpID) - successfulID, token, changeDate, sequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID.Id, "", "id") - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID.Id, "user", "id") - ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Instance.CreateSuccessfulLDAPIntent(t, CTX, idpID.Id, "", "id") - ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Instance.CreateSuccessfulLDAPIntent(t, CTX, idpID.Id, "user", "id") - samlSuccessfulID, samlToken, samlChangeDate, samlSequence := Instance.CreateSuccessfulSAMLIntent(t, CTX, idpID.Id, "", "id") + oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() + oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() + samlIdpID := Instance.AddSAMLPostProvider(IamCTX) + ldapIdpID := Instance.AddLDAPProvider(IamCTX) + authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) + require.NoError(t, err) + intentID := authURL.Query().Get("state") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "") + require.NoError(t, err) + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user") + require.NoError(t, err) + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") + require.NoError(t, err) + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + require.NoError(t, err) + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") + require.NoError(t, err) + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") + require.NoError(t, err) + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "") + require.NoError(t, err) + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user") + require.NoError(t, err) type args struct { ctx context.Context req *user.RetrieveIdentityProviderIntentRequest @@ -2184,7 +2203,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { wantErr: true, }, { - name: "retrieve successful intent", + name: "retrieve successful oauth intent", args: args{ CTX, &user.RetrieveIdentityProviderIntentRequest{ @@ -2205,13 +2224,15 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID.Id, + IdpId: oauthIdpID, UserId: "id", - UserName: "username", + UserName: "", RawInformation: func() *structpb.Struct { s, err := structpb.NewStruct(map[string]interface{}{ - "sub": "id", - "preferred_username": "username", + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, }) require.NoError(t, err) return s @@ -2243,7 +2264,85 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID.Id, + IdpId: oauthIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessful, + IdpIntentToken: oidcToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcSequence, + }, + UserId: "", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessfulWithUserID, + IdpIntentToken: oidcWithUserIDToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcWithUserIDChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcWithUserIDSequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2287,7 +2386,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID.Id, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2333,7 +2432,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID.Id, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2370,7 +2469,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { Assertion: []byte(""), }, }, - IdpId: idpID.Id, + IdpId: samlIdpID, UserId: "id", UserName: "", RawInformation: func() *structpb.Struct { @@ -2387,6 +2486,45 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful saml intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: samlSuccessfulWithUserID, + IdpIntentToken: samlWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(samlWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: samlWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: []byte(""), + }, + }, + IdpId: samlIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "attributes": map[string]interface{}{ + "attribute1": []interface{}{"value1"}, + }, + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2401,7 +2539,6 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }) } } -*/ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { userIDWithoutAuth := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2beta/passkey_test.go b/internal/api/grpc/user/v2beta/passkey_test.go index 7d45c41756..f4a48ed941 100644 --- a/internal/api/grpc/user/v2beta/passkey_test.go +++ b/internal/api/grpc/user/v2beta/passkey_test.go @@ -74,6 +74,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, ID: "123", @@ -90,6 +91,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, ID: "123", @@ -100,6 +102,10 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { want: &user.RegisterPasskeyResponse{ Details: &object.Details{ Sequence: 22, + CreationDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, ChangeDate: ×tamppb.Timestamp{ Seconds: 3000, Nanos: 22, @@ -150,6 +156,7 @@ func Test_passkeyDetailsToPb(t *testing.T) { details: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, err: nil, @@ -157,6 +164,10 @@ func Test_passkeyDetailsToPb(t *testing.T) { want: &user.CreatePasskeyRegistrationLinkResponse{ Details: &object.Details{ Sequence: 22, + CreationDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, ChangeDate: ×tamppb.Timestamp{ Seconds: 3000, Nanos: 22, @@ -199,6 +210,7 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, CodeID: "123", @@ -209,6 +221,10 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) { want: &user.CreatePasskeyRegistrationLinkResponse{ Details: &object.Details{ Sequence: 22, + CreationDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, ChangeDate: ×tamppb.Timestamp{ Seconds: 3000, Nanos: 22, diff --git a/internal/api/grpc/user/v2beta/u2f_test.go b/internal/api/grpc/user/v2beta/u2f_test.go index 087837ce3c..53f2a0bb8c 100644 --- a/internal/api/grpc/user/v2beta/u2f_test.go +++ b/internal/api/grpc/user/v2beta/u2f_test.go @@ -43,6 +43,7 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, ID: "123", @@ -59,6 +60,7 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), + CreationDate: time.Unix(3000, 22), ResourceOwner: "me", }, ID: "123", @@ -69,6 +71,10 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) { want: &user.RegisterU2FResponse{ Details: &object.Details{ Sequence: 22, + CreationDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, ChangeDate: ×tamppb.Timestamp{ Seconds: 3000, Nanos: 22, diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index 52da057906..cf6dfa6304 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -371,31 +371,31 @@ func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.Star } func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { - intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID()) + state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) if err != nil { return nil, err } - content, redirect, err := s.command.AuthFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) + _, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters()) if err != nil { return nil, err } + content, redirect := session.GetAuth(ctx) if redirect { return &user.StartIdentityProviderIntentResponse{ Details: object.DomainToDetailsPb(details), NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, }, nil - } else { - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ - PostForm: []byte(content), - }, - }, nil } + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ + PostForm: []byte(content), + }, + }, nil } func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { - intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetInstance(ctx).InstanceID()) + intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go similarity index 65% rename from internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go rename to internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go index eafa733fd1..002669c233 100644 --- a/internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go +++ b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go @@ -17,9 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) var ( @@ -37,7 +35,7 @@ func TestMain(m *testing.M) { func TestServer_Feature_Disabled(t *testing.T) { instance, iamCtx, _ := createInstance(t, false) - client := instance.Client.WebKeyV3Alpha + client := instance.Client.WebKeyV2Beta t.Run("CreateWebKey", func(t *testing.T) { _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{}) @@ -65,84 +63,78 @@ func TestServer_ListWebKeys(t *testing.T) { instance, iamCtx, creationDate := createInstance(t, true) // After the feature is first enabled, we can expect 2 generated keys with the default config. checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, creationDate) } func TestServer_CreateWebKey(t *testing.T) { instance, iamCtx, creationDate := createInstance(t, true) - client := instance.Client.WebKeyV3Alpha + client := instance.Client.WebKeyV2Beta _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, - }, + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, }) require.NoError(t, err) checkWebKeyListState(iamCtx, t, instance, 3, "", &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, creationDate) } func TestServer_ActivateWebKey(t *testing.T) { instance, iamCtx, creationDate := createInstance(t, true) - client := instance.Client.WebKeyV3Alpha + client := instance.Client.WebKeyV2Beta resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, - }, + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, }) require.NoError(t, err) _, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ - Id: resp.GetDetails().GetId(), + Id: resp.GetId(), }) require.NoError(t, err) - checkWebKeyListState(iamCtx, t, instance, 3, resp.GetDetails().GetId(), &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + checkWebKeyListState(iamCtx, t, instance, 3, resp.GetId(), &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, creationDate) } func TestServer_DeleteWebKey(t *testing.T) { instance, iamCtx, creationDate := createInstance(t, true) - client := instance.Client.WebKeyV3Alpha + client := instance.Client.WebKeyV2Beta keyIDs := make([]string, 2) for i := 0; i < 2; i++ { resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, - }, + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, }) require.NoError(t, err) - keyIDs[i] = resp.GetDetails().GetId() + keyIDs[i] = resp.GetId() } _, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ Id: keyIDs[0], @@ -162,11 +154,35 @@ func TestServer_DeleteWebKey(t *testing.T) { return } + start := time.Now() ok = t.Run("delete inactive key", func(t *testing.T) { - _, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ Id: keyIDs[1], }) require.NoError(t, err) + require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now()) + }) + if !ok { + return + } + + ok = t.Run("delete inactive key again", func(t *testing.T) { + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: keyIDs[1], + }) + require.NoError(t, err) + require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now()) + }) + if !ok { + return + } + + ok = t.Run("delete not existing key", func(t *testing.T) { + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: "not-existing", + }) + require.NoError(t, err) + require.Nil(t, resp.DeletionDate) }) if !ok { return @@ -174,9 +190,9 @@ func TestServer_DeleteWebKey(t *testing.T) { // There are 2 keys from feature setup, +2 created, -1 deleted = 3 checkWebKeyListState(iamCtx, t, instance, 3, keyIDs[0], &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, creationDate) } @@ -195,7 +211,7 @@ func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, co retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute) assert.EventuallyWithT(t, func(collect *assert.CollectT) { - resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{}) + resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{}) if enableFeature { assert.NoError(collect, err) assert.Len(collect, resp.GetWebKeys(), 2) @@ -220,7 +236,7 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) assert.EventuallyWithT(t, func(collect *assert.CollectT) { - resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(ctx, &webkey.ListWebKeysRequest{}) + resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(ctx, &webkey.ListWebKeysRequest{}) require.NoError(collect, err) list := resp.GetWebKeys() assert.Len(collect, list, nKeys) @@ -228,21 +244,14 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati now := time.Now() var gotActiveKeyID string for _, key := range list { - integration.AssertResourceDetails(t, &resource_object.Details{ - Created: creationDate, - Changed: creationDate, - Owner: &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: instance.ID(), - }, - }, key.GetDetails()) - assert.WithinRange(collect, key.GetDetails().GetChanged().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) - assert.NotEqual(collect, webkey.WebKeyState_STATE_UNSPECIFIED, key.GetState()) - assert.NotEqual(collect, webkey.WebKeyState_STATE_REMOVED, key.GetState()) - assert.Equal(collect, config, key.GetConfig().GetConfig()) + assert.WithinRange(collect, key.GetCreationDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) + assert.WithinRange(collect, key.GetChangeDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) + assert.NotEqual(collect, webkey.State_STATE_UNSPECIFIED, key.GetState()) + assert.NotEqual(collect, webkey.State_STATE_REMOVED, key.GetState()) + assert.Equal(collect, config, key.GetKey()) - if key.GetState() == webkey.WebKeyState_STATE_ACTIVE { - gotActiveKeyID = key.GetDetails().GetId() + if key.GetState() == webkey.State_STATE_ACTIVE { + gotActiveKeyID = key.GetId() } } assert.NotEmpty(collect, gotActiveKeyID) diff --git a/internal/api/grpc/resources/webkey/v3/server.go b/internal/api/grpc/webkey/v2beta/server.go similarity index 67% rename from internal/api/grpc/resources/webkey/v3/server.go rename to internal/api/grpc/webkey/v2beta/server.go index 4e97965932..0d4ddb19c8 100644 --- a/internal/api/grpc/resources/webkey/v3/server.go +++ b/internal/api/grpc/webkey/v2beta/server.go @@ -7,11 +7,11 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) type Server struct { - webkey.UnimplementedZITADELWebKeysServer + webkey.UnimplementedWebKeyServiceServer command *command.Commands query *query.Queries } @@ -27,21 +27,21 @@ func CreateServer( } func (s *Server) RegisterServer(grpcServer *grpc.Server) { - webkey.RegisterZITADELWebKeysServer(grpcServer, s) + webkey.RegisterWebKeyServiceServer(grpcServer, s) } func (s *Server) AppName() string { - return webkey.ZITADELWebKeys_ServiceDesc.ServiceName + return webkey.WebKeyService_ServiceDesc.ServiceName } func (s *Server) MethodPrefix() string { - return webkey.ZITADELWebKeys_ServiceDesc.ServiceName + return webkey.WebKeyService_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { - return webkey.ZITADELWebKeys_AuthMethods + return webkey.WebKeyService_AuthMethods } func (s *Server) RegisterGateway() server.RegisterGatewayFunc { - return webkey.RegisterZITADELWebKeysHandler + return webkey.RegisterWebKeyServiceHandler } diff --git a/internal/api/grpc/resources/webkey/v3/webkey.go b/internal/api/grpc/webkey/v2beta/webkey.go similarity index 72% rename from internal/api/grpc/resources/webkey/v3/webkey.go rename to internal/api/grpc/webkey/v2beta/webkey.go index 8a6e72f950..d45288dff2 100644 --- a/internal/api/grpc/resources/webkey/v3/webkey.go +++ b/internal/api/grpc/webkey/v2beta/webkey.go @@ -3,12 +3,12 @@ package webkey import ( "context" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/api/authz" - resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) { @@ -24,7 +24,8 @@ func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyReque } return &webkey.CreateWebKeyResponse{ - Details: resource_object.DomainToDetailsPb(webKey.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + Id: webKey.KeyID, + CreationDate: timestamppb.New(webKey.ObjectDetails.EventDate), }, nil } @@ -41,7 +42,7 @@ func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyR } return &webkey.ActivateWebKeyResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + ChangeDate: timestamppb.New(details.EventDate), }, nil } @@ -52,13 +53,17 @@ func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyReque if err = checkWebKeyFeature(ctx); err != nil { return nil, err } - details, err := s.command.DeleteWebKey(ctx, req.GetId()) + deletedAt, err := s.command.DeleteWebKey(ctx, req.GetId()) if err != nil { return nil, err } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } return &webkey.DeleteWebKeyResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + DeletionDate: deletionDate, }, nil } @@ -75,7 +80,7 @@ func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) } return &webkey.ListWebKeysResponse{ - WebKeys: webKeyDetailsListToPb(list, authz.GetInstance(ctx).InstanceID()), + WebKeys: webKeyDetailsListToPb(list), }, nil } diff --git a/internal/api/grpc/webkey/v2beta/webkey_converter.go b/internal/api/grpc/webkey/v2beta/webkey_converter.go new file mode 100644 index 0000000000..ac8939470b --- /dev/null +++ b/internal/api/grpc/webkey/v2beta/webkey_converter.go @@ -0,0 +1,170 @@ +package webkey + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" +) + +func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig { + switch config := req.GetKey().(type) { + case *webkey.CreateWebKeyRequest_Rsa: + return rsaToCrypto(config.Rsa) + case *webkey.CreateWebKeyRequest_Ecdsa: + return ecdsaToCrypto(config.Ecdsa) + case *webkey.CreateWebKeyRequest_Ed25519: + return new(crypto.WebKeyED25519Config) + default: + return rsaToCrypto(nil) + } +} + +func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig { + out := new(crypto.WebKeyRSAConfig) + + switch config.GetBits() { + case webkey.RSABits_RSA_BITS_UNSPECIFIED: + out.Bits = crypto.RSABits2048 + case webkey.RSABits_RSA_BITS_2048: + out.Bits = crypto.RSABits2048 + case webkey.RSABits_RSA_BITS_3072: + out.Bits = crypto.RSABits3072 + case webkey.RSABits_RSA_BITS_4096: + out.Bits = crypto.RSABits4096 + default: + out.Bits = crypto.RSABits2048 + } + + switch config.GetHasher() { + case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED: + out.Hasher = crypto.RSAHasherSHA256 + case webkey.RSAHasher_RSA_HASHER_SHA256: + out.Hasher = crypto.RSAHasherSHA256 + case webkey.RSAHasher_RSA_HASHER_SHA384: + out.Hasher = crypto.RSAHasherSHA384 + case webkey.RSAHasher_RSA_HASHER_SHA512: + out.Hasher = crypto.RSAHasherSHA512 + default: + out.Hasher = crypto.RSAHasherSHA256 + } + + return out +} + +func ecdsaToCrypto(config *webkey.ECDSA) *crypto.WebKeyECDSAConfig { + out := new(crypto.WebKeyECDSAConfig) + + switch config.GetCurve() { + case webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED: + out.Curve = crypto.EllipticCurveP256 + case webkey.ECDSACurve_ECDSA_CURVE_P256: + out.Curve = crypto.EllipticCurveP256 + case webkey.ECDSACurve_ECDSA_CURVE_P384: + out.Curve = crypto.EllipticCurveP384 + case webkey.ECDSACurve_ECDSA_CURVE_P512: + out.Curve = crypto.EllipticCurveP512 + default: + out.Curve = crypto.EllipticCurveP256 + } + + return out +} + +func webKeyDetailsListToPb(list []query.WebKeyDetails) []*webkey.WebKey { + out := make([]*webkey.WebKey, len(list)) + for i := range list { + out[i] = webKeyDetailsToPb(&list[i]) + } + return out +} + +func webKeyDetailsToPb(details *query.WebKeyDetails) *webkey.WebKey { + out := &webkey.WebKey{ + Id: details.KeyID, + CreationDate: timestamppb.New(details.CreationDate), + ChangeDate: timestamppb.New(details.ChangeDate), + State: webKeyStateToPb(details.State), + } + + switch config := details.Config.(type) { + case *crypto.WebKeyRSAConfig: + out.Key = &webkey.WebKey_Rsa{ + Rsa: webKeyRSAConfigToPb(config), + } + case *crypto.WebKeyECDSAConfig: + out.Key = &webkey.WebKey_Ecdsa{ + Ecdsa: webKeyECDSAConfigToPb(config), + } + case *crypto.WebKeyED25519Config: + out.Key = &webkey.WebKey_Ed25519{ + Ed25519: new(webkey.ED25519), + } + } + + return out +} + +func webKeyStateToPb(state domain.WebKeyState) webkey.State { + switch state { + case domain.WebKeyStateUnspecified: + return webkey.State_STATE_UNSPECIFIED + case domain.WebKeyStateInitial: + return webkey.State_STATE_INITIAL + case domain.WebKeyStateActive: + return webkey.State_STATE_ACTIVE + case domain.WebKeyStateInactive: + return webkey.State_STATE_INACTIVE + case domain.WebKeyStateRemoved: + return webkey.State_STATE_REMOVED + default: + return webkey.State_STATE_UNSPECIFIED + } +} + +func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.RSA { + out := new(webkey.RSA) + + switch config.Bits { + case crypto.RSABitsUnspecified: + out.Bits = webkey.RSABits_RSA_BITS_UNSPECIFIED + case crypto.RSABits2048: + out.Bits = webkey.RSABits_RSA_BITS_2048 + case crypto.RSABits3072: + out.Bits = webkey.RSABits_RSA_BITS_3072 + case crypto.RSABits4096: + out.Bits = webkey.RSABits_RSA_BITS_4096 + } + + switch config.Hasher { + case crypto.RSAHasherUnspecified: + out.Hasher = webkey.RSAHasher_RSA_HASHER_UNSPECIFIED + case crypto.RSAHasherSHA256: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA256 + case crypto.RSAHasherSHA384: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA384 + case crypto.RSAHasherSHA512: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA512 + } + + return out +} + +func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.ECDSA { + out := new(webkey.ECDSA) + + switch config.Curve { + case crypto.EllipticCurveUnspecified: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED + case crypto.EllipticCurveP256: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P256 + case crypto.EllipticCurveP384: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P384 + case crypto.EllipticCurveP512: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P512 + } + + return out +} diff --git a/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go b/internal/api/grpc/webkey/v2beta/webkey_converter_test.go similarity index 56% rename from internal/api/grpc/resources/webkey/v3/webkey_converter_test.go rename to internal/api/grpc/webkey/v2beta/webkey_converter_test.go index e755d2be08..d78e9968dc 100644 --- a/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go +++ b/internal/api/grpc/webkey/v2beta/webkey_converter_test.go @@ -10,9 +10,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" - webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) func Test_createWebKeyRequestToConfig(t *testing.T) { @@ -27,12 +25,10 @@ func Test_createWebKeyRequestToConfig(t *testing.T) { { name: "RSA", args: args{&webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, - }, + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }, }, }}, @@ -44,11 +40,9 @@ func Test_createWebKeyRequestToConfig(t *testing.T) { { name: "ECDSA", args: args{&webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Ecdsa{ - Ecdsa: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, - }, + Key: &webkey.CreateWebKeyRequest_Ecdsa{ + Ecdsa: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, }, }, }}, @@ -59,10 +53,8 @@ func Test_createWebKeyRequestToConfig(t *testing.T) { { name: "ED25519", args: args{&webkey.CreateWebKeyRequest{ - Key: &webkey.WebKey{ - Config: &webkey.WebKey_Ed25519{ - Ed25519: &webkey.WebKeyED25519Config{}, - }, + Key: &webkey.CreateWebKeyRequest_Ed25519{ + Ed25519: &webkey.ED25519{}, }, }}, want: &crypto.WebKeyED25519Config{}, @@ -86,7 +78,7 @@ func Test_createWebKeyRequestToConfig(t *testing.T) { func Test_webKeyRSAConfigToCrypto(t *testing.T) { type args struct { - config *webkey.WebKeyRSAConfig + config *webkey.RSA } tests := []struct { name string @@ -95,9 +87,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }{ { name: "unspecified", - args: args{&webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED, + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_UNSPECIFIED, + Hasher: webkey.RSAHasher_RSA_HASHER_UNSPECIFIED, }}, want: &crypto.WebKeyRSAConfig{ Bits: crypto.RSABits2048, @@ -106,9 +98,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }, { name: "2048, RSA256", - args: args{&webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }}, want: &crypto.WebKeyRSAConfig{ Bits: crypto.RSABits2048, @@ -117,9 +109,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }, { name: "3072, RSA384", - args: args{&webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }}, want: &crypto.WebKeyRSAConfig{ Bits: crypto.RSABits3072, @@ -128,9 +120,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }, { name: "4096, RSA512", - args: args{&webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512, + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_4096, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA512, }}, want: &crypto.WebKeyRSAConfig{ Bits: crypto.RSABits4096, @@ -139,7 +131,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { }, { name: "invalid", - args: args{&webkey.WebKeyRSAConfig{ + args: args{&webkey.RSA{ Bits: 99, Hasher: 99, }}, @@ -151,7 +143,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := webKeyRSAConfigToCrypto(tt.args.config) + got := rsaToCrypto(tt.args.config) assert.Equal(t, tt.want, got) }) } @@ -159,7 +151,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) { func Test_webKeyECDSAConfigToCrypto(t *testing.T) { type args struct { - config *webkey.WebKeyECDSAConfig + config *webkey.ECDSA } tests := []struct { name string @@ -168,8 +160,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }{ { name: "unspecified", - args: args{&webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED, + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED, }}, want: &crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP256, @@ -177,8 +169,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }, { name: "P256", - args: args{&webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256, + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P256, }}, want: &crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP256, @@ -186,8 +178,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }, { name: "P384", - args: args{&webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, }}, want: &crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP384, @@ -195,8 +187,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }, { name: "P512", - args: args{&webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512, + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P512, }}, want: &crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP512, @@ -204,7 +196,7 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { }, { name: "invalid", - args: args{&webkey.WebKeyECDSAConfig{ + args: args{&webkey.ECDSA{ Curve: 99, }}, want: &crypto.WebKeyECDSAConfig{ @@ -214,14 +206,13 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := webKeyECDSAConfigToCrypto(tt.args.config) + got := ecdsaToCrypto(tt.args.config) assert.Equal(t, tt.want, got) }) } } func Test_webKeyDetailsListToPb(t *testing.T) { - instanceID := "ownerid" list := []query.WebKeyDetails{ { KeyID: "key1", @@ -243,52 +234,41 @@ func Test_webKeyDetailsListToPb(t *testing.T) { Config: &crypto.WebKeyED25519Config{}, }, } - want := []*webkey.GetWebKey{ + want := []*webkey.WebKey{ { - Details: &resource_object.Details{ - Id: "key1", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, - }, + Id: "key1", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }, }, }, { - Details: &resource_object.Details{ - Id: "key2", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Ed25519{ - Ed25519: &webkey.WebKeyED25519Config{}, - }, + Id: "key2", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.ED25519{}, }, }, } - got := webKeyDetailsListToPb(list, instanceID) + got := webKeyDetailsListToPb(list) assert.Equal(t, want, got) } func Test_webKeyDetailsToPb(t *testing.T) { - instanceID := "ownerid" type args struct { details *query.WebKeyDetails } tests := []struct { name string args args - want *webkey.GetWebKey + want *webkey.WebKey }{ { name: "RSA", @@ -303,20 +283,15 @@ func Test_webKeyDetailsToPb(t *testing.T) { Hasher: crypto.RSAHasherSHA384, }, }}, - want: &webkey.GetWebKey{ - Details: &resource_object.Details{ - Id: "keyID", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Rsa{ - Rsa: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, - }, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }, }, }, @@ -333,19 +308,14 @@ func Test_webKeyDetailsToPb(t *testing.T) { Curve: crypto.EllipticCurveP384, }, }}, - want: &webkey.GetWebKey{ - Details: &resource_object.Details{ - Id: "keyID", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Ecdsa{ - Ecdsa: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, - }, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ecdsa{ + Ecdsa: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, }, }, }, @@ -360,25 +330,20 @@ func Test_webKeyDetailsToPb(t *testing.T) { State: domain.WebKeyStateActive, Config: &crypto.WebKeyED25519Config{}, }}, - want: &webkey.GetWebKey{ - Details: &resource_object.Details{ - Id: "keyID", - Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, - Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, - Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, - }, - State: webkey.WebKeyState_STATE_ACTIVE, - Config: &webkey.WebKey{ - Config: &webkey.WebKey_Ed25519{ - Ed25519: &webkey.WebKeyED25519Config{}, - }, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.ED25519{}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := webKeyDetailsToPb(tt.args.details, instanceID) + got := webKeyDetailsToPb(tt.args.details) assert.Equal(t, tt.want, got) }) } @@ -391,37 +356,37 @@ func Test_webKeyStateToPb(t *testing.T) { tests := []struct { name string args args - want webkey.WebKeyState + want webkey.State }{ { name: "unspecified", args: args{domain.WebKeyStateUnspecified}, - want: webkey.WebKeyState_STATE_UNSPECIFIED, + want: webkey.State_STATE_UNSPECIFIED, }, { name: "initial", args: args{domain.WebKeyStateInitial}, - want: webkey.WebKeyState_STATE_INITIAL, + want: webkey.State_STATE_INITIAL, }, { name: "active", args: args{domain.WebKeyStateActive}, - want: webkey.WebKeyState_STATE_ACTIVE, + want: webkey.State_STATE_ACTIVE, }, { name: "inactive", args: args{domain.WebKeyStateInactive}, - want: webkey.WebKeyState_STATE_INACTIVE, + want: webkey.State_STATE_INACTIVE, }, { name: "removed", args: args{domain.WebKeyStateRemoved}, - want: webkey.WebKeyState_STATE_REMOVED, + want: webkey.State_STATE_REMOVED, }, { name: "invalid", args: args{99}, - want: webkey.WebKeyState_STATE_UNSPECIFIED, + want: webkey.State_STATE_UNSPECIFIED, }, } for _, tt := range tests { @@ -439,7 +404,7 @@ func Test_webKeyRSAConfigToPb(t *testing.T) { tests := []struct { name string args args - want *webkey.WebKeyRSAConfig + want *webkey.RSA }{ { name: "2048, RSA256", @@ -447,9 +412,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) { Bits: crypto.RSABits2048, Hasher: crypto.RSAHasherSHA256, }}, - want: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, }, }, { @@ -458,9 +423,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) { Bits: crypto.RSABits3072, Hasher: crypto.RSAHasherSHA384, }}, - want: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, }, }, { @@ -469,9 +434,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) { Bits: crypto.RSABits4096, Hasher: crypto.RSAHasherSHA512, }}, - want: &webkey.WebKeyRSAConfig{ - Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096, - Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_4096, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA512, }, }, } @@ -490,15 +455,15 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) { tests := []struct { name string args args - want *webkey.WebKeyECDSAConfig + want *webkey.ECDSA }{ { name: "P256", args: args{&crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP256, }}, - want: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P256, }, }, { @@ -506,8 +471,8 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) { args: args{&crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP384, }}, - want: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, }, }, { @@ -515,8 +480,8 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) { args: args{&crypto.WebKeyECDSAConfig{ Curve: crypto.EllipticCurveP512, }}, - want: &webkey.WebKeyECDSAConfig{ - Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P512, }, }, } diff --git a/internal/api/http/middleware/auth_interceptor.go b/internal/api/http/middleware/auth_interceptor.go index 1581d401b4..ae9377b13d 100644 --- a/internal/api/http/middleware/auth_interceptor.go +++ b/internal/api/http/middleware/auth_interceptor.go @@ -14,14 +14,16 @@ import ( ) type AuthInterceptor struct { - verifier authz.APITokenVerifier - authConfig authz.Config + verifier authz.APITokenVerifier + authConfig authz.Config + systemAuthConfig authz.Config } -func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) *AuthInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemAuthConfig authz.Config, authConfig authz.Config) *AuthInterceptor { return &AuthInterceptor{ - verifier: verifier, - authConfig: authConfig, + verifier: verifier, + authConfig: authConfig, + systemAuthConfig: systemAuthConfig, } } @@ -31,7 +33,7 @@ func (a *AuthInterceptor) Handler(next http.Handler) http.Handler { func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, err := authorize(r, a.verifier, a.authConfig) + ctx, err := authorize(r, a.verifier, a.systemAuthConfig, a.authConfig) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return @@ -44,7 +46,7 @@ func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc { func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) HandlerFuncWithError { return func(w http.ResponseWriter, r *http.Request) error { - ctx, err := authorize(r, a.verifier, a.authConfig) + ctx, err := authorize(r, a.verifier, a.systemAuthConfig, a.authConfig) if err != nil { return err } @@ -56,7 +58,7 @@ func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) Handle type httpReq struct{} -func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig authz.Config) (_ context.Context, err error) { +func authorize(r *http.Request, verifier authz.APITokenVerifier, systemAuthConfig authz.Config, authConfig authz.Config) (_ context.Context, err error) { ctx := r.Context() authOpt, needsToken := checkAuthMethod(r, verifier) @@ -71,7 +73,7 @@ func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig auth return nil, zerrors.ThrowUnauthenticated(nil, "AUT-1179", "auth header missing") } - ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, authConfig, authOpt, r.RequestURI) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, systemAuthConfig.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, r.RequestURI) if err != nil { return nil, err } diff --git a/internal/api/http/middleware/middleware_test.go b/internal/api/http/middleware/middleware_test.go index 4d7cb6636d..60d4099e06 100644 --- a/internal/api/http/middleware/middleware_test.go +++ b/internal/api/http/middleware/middleware_test.go @@ -1,6 +1,7 @@ package middleware import ( + "os" "testing" "golang.org/x/text/language" @@ -14,5 +15,5 @@ var ( func TestMain(m *testing.M) { i18n.SupportLanguages(SupportedLanguages...) - m.Run() + os.Exit(m.Run()) } diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index 01594c43ba..c3e9586a59 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -328,7 +328,7 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { return } - idpUser, idpSession, err := h.fetchIDPUserFromCode(ctx, provider, data.Code, data.User) + idpUser, idpSession, err := h.fetchIDPUserFromCode(ctx, provider, data.Code, data.User, intent.IDPArguments) if err != nil { cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error()) logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") @@ -410,23 +410,23 @@ func redirectToFailureURL(w http.ResponseWriter, r *http.Request, i *command.IDP http.Redirect(w, r, i.FailureURL.String(), http.StatusFound) } -func (h *Handler) fetchIDPUserFromCode(ctx context.Context, identityProvider idp.Provider, code string, appleUser string) (user idp.User, idpTokens idp.Session, err error) { +func (h *Handler) fetchIDPUserFromCode(ctx context.Context, identityProvider idp.Provider, code string, appleUser string, idpArguments map[string]any) (user idp.User, idpTokens idp.Session, err error) { var session idp.Session switch provider := identityProvider.(type) { case *oauth.Provider: - session = &oauth.Session{Provider: provider, Code: code} + session = oauth.NewSession(provider, code, idpArguments) case *openid.Provider: - session = &openid.Session{Provider: provider, Code: code} + session = openid.NewSession(provider, code, idpArguments) case *azuread.Provider: - session = &azuread.Session{Provider: provider, Code: code} + session = azuread.NewSession(provider, code) case *github.Provider: - session = &oauth.Session{Provider: provider.Provider, Code: code} + session = oauth.NewSession(provider.Provider, code, idpArguments) case *gitlab.Provider: - session = &openid.Session{Provider: provider.Provider, Code: code} + session = openid.NewSession(provider.Provider, code, idpArguments) case *google.Provider: - session = &openid.Session{Provider: provider.Provider, Code: code} + session = openid.NewSession(provider.Provider, code, idpArguments) case *apple.Provider: - session = &apple.Session{Session: &openid.Session{Provider: provider.Provider, Code: code}, UserFormValue: appleUser} + session = apple.NewSession(provider, code, appleUser) case *jwt.Provider, *ldap.Provider, *saml2.Provider: return nil, nil, zerrors.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented") default: diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 793001045c..f750b2a3ea 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -111,6 +111,7 @@ func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc. Prompt: PromptToBusiness(req.Prompt), UILocales: UILocalesToBusiness(req.UILocales), MaxAge: MaxAgeToBusiness(req.MaxAge), + Issuer: o.contextToIssuer(ctx), } if req.LoginHint != "" { authRequest.LoginHint = &req.LoginHint @@ -149,7 +150,7 @@ func (o *OPStorage) audienceFromProjectID(ctx context.Context, projectID string) if err != nil { return nil, err } - appIDs, err := o.query.SearchClientIDs(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, true) + appIDs, err := o.query.SearchClientIDs(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, false) if err != nil { return nil, err } @@ -545,11 +546,7 @@ func CreateCodeCallbackURL(ctx context.Context, authReq op.AuthRequest, authoriz code: code, state: authReq.GetState(), } - callback, err := op.AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) - if err != nil { - return "", err - } - return callback, err + return op.AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) } func (s *Server) CreateTokenCallbackURL(ctx context.Context, req op.AuthRequest) (string, error) { @@ -568,6 +565,7 @@ func (s *Server) CreateTokenCallbackURL(ctx context.Context, req op.AuthRequest) req.GetID(), implicitFlowComplianceChecker(), slices.Contains(client.GrantTypes(), oidc.GrantTypeRefreshToken), + client.client.BackChannelLogoutURI, ) if err != nil { return "", err diff --git a/internal/api/oidc/integration_test/token_device_test.go b/internal/api/oidc/integration_test/token_device_test.go new file mode 100644 index 0000000000..0c6a65e8a2 --- /dev/null +++ b/internal/api/oidc/integration_test/token_device_test.go @@ -0,0 +1,127 @@ +//go:build integration + +package oidc_test + +import ( + "context" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/app" + "github.com/zitadel/zitadel/pkg/grpc/auth" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" +) + +func TestServer_DeviceAuth(t *testing.T) { + project, err := Instance.CreateProject(CTX) + require.NoError(t, err) + client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) + require.NoError(t, err) + + tests := []struct { + name string + scope []string + decision func(t *testing.T, id string) + wantErr error + }{ + { + name: "authorized", + scope: []string{}, + decision: func(t *testing.T, id string) { + sessionID, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) + _, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: id, + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionID, + SessionToken: sessionToken, + }, + }, + }) + require.NoError(t, err) + }, + }, + { + name: "authorized, with ZITADEL", + scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL}, + decision: func(t *testing.T, id string) { + sessionID, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) + _, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: id, + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionID, + SessionToken: sessionToken, + }, + }, + }) + require.NoError(t, err) + }, + }, + { + name: "denied", + scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL}, + decision: func(t *testing.T, id string) { + _, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: id, + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{ + Deny: &oidc_pb.Deny{}, + }, + }) + require.NoError(t, err) + }, + wantErr: oidc.ErrAccessDenied(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), client.GetClientId(), "", "", tt.scope) + require.NoError(t, err) + deviceAuthorization, err := rp.DeviceAuthorization(CTX, tt.scope, provider, nil) + require.NoError(t, err) + + relyingPartyDone := make(chan struct{}) + go func() { + ctx, cancel := context.WithTimeout(CTX, 1*time.Minute) + defer func() { + cancel() + relyingPartyDone <- struct{}{} + }() + tokens, err := rp.DeviceAccessToken(ctx, deviceAuthorization.DeviceCode, time.Duration(deviceAuthorization.Interval)*time.Second, provider) + require.ErrorIs(t, err, tt.wantErr) + + if tokens == nil { + return + } + _, err = Instance.Client.Auth.GetMyUser(integration.WithAuthorizationToken(CTX, tokens.AccessToken), &auth.GetMyUserRequest{}) + if slices.Contains(tt.scope, domain.ProjectScopeZITADEL) { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }() + + var req *oidc_pb.GetDeviceAuthorizationRequestResponse + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(collectT *assert.CollectT) { + req, err = Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: deviceAuthorization.UserCode, + }) + assert.NoError(collectT, err) + }, retryDuration, tick) + + tt.decision(t, req.GetDeviceAuthorizationRequest().GetId()) + + <-relyingPartyDone + }) + } +} diff --git a/internal/api/oidc/integration_test/token_exchange_test.go b/internal/api/oidc/integration_test/token_exchange_test.go index 1319eea19a..0844898a2f 100644 --- a/internal/api/oidc/integration_test/token_exchange_test.go +++ b/internal/api/oidc/integration_test/token_exchange_test.go @@ -428,6 +428,17 @@ func TestServer_TokenExchangeImpersonation(t *testing.T) { }, wantErr: true, }, + { + name: "IMPERSONATION: subject: userID, actor: access token, requested type: JWT, membership not found error", + args: args{ + SubjectToken: userResp.GetUserId(), + SubjectTokenType: oidc_api.UserIDTokenType, + RequestedTokenType: oidc.JWTTokenType, + ActorToken: noPermPAT, + ActorTokenType: oidc.AccessTokenType, + }, + wantErr: true, + }, { name: "IAM IMPERSONATION: subject: userID, actor: access token, success", args: args{ diff --git a/internal/api/oidc/integration_test/token_jwt_profile_test.go b/internal/api/oidc/integration_test/token_jwt_profile_test.go index 5845713317..b4ae0d777a 100644 --- a/internal/api/oidc/integration_test/token_jwt_profile_test.go +++ b/internal/api/oidc/integration_test/token_jwt_profile_test.go @@ -21,7 +21,9 @@ import ( ) func TestServer_JWTProfile(t *testing.T) { - user, name, keyData, err := Instance.CreateOIDCJWTProfileClient(CTX) + user, name, keyData, err := Instance.CreateOIDCJWTProfileClient(CTX, time.Hour) + require.NoError(t, err) + _, _, keyDataExpired, err := Instance.CreateOIDCJWTProfileClient(CTX, 10*time.Second) require.NoError(t, err) type claims struct { @@ -104,6 +106,12 @@ func TestServer_JWTProfile(t *testing.T) { resourceOwnerPrimaryDomain: Instance.DefaultOrg.PrimaryDomain, }, }, + { + name: "key expired", + keyData: keyDataExpired, + scope: []string{oidc.ScopeOpenID}, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -123,6 +131,9 @@ func TestServer_JWTProfile(t *testing.T) { }, time.Minute, time.Second, ) + if tt.wantErr { + return + } provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), "", "", redirectURI, tt.scope) require.NoError(t, err) diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 6c0599f556..81f3b1c466 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -417,15 +417,15 @@ func (o *OPStorage) getMaxKeySequence(ctx context.Context) (float64, error) { eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). - AllowTimeTravel(). AddQuery(). - AggregateTypes(keypair.AggregateType). + AggregateTypes( + keypair.AggregateType, + instance.AggregateType, + ). EventTypes( keypair.AddedEventType, + instance.InstanceRemovedEventType, ). - Or(). - AggregateTypes(instance.AggregateType). - EventTypes(instance.InstanceRemovedEventType). Builder(), ) } diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 153a13f06e..37a9ba2bce 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -75,6 +75,7 @@ type OPStorage struct { encAlg crypto.EncryptionAlgorithm locker crdb.Locker assetAPIPrefix func(ctx context.Context) string + contextToIssuer func(context.Context) string } // Provider is used to overload certain [op.Provider] methods @@ -119,7 +120,7 @@ func NewServer( if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } - storage := newStorage(config, command, query, repo, encryptionAlg, es, projections) + storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, ContextToIssuer) keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query)) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) @@ -182,9 +183,13 @@ func NewServer( return server, nil } +func ContextToIssuer(ctx context.Context) string { + return http_utils.DomainContext(ctx).Origin() +} + func IssuerFromContext(_ bool) (op.IssuerFromRequest, error) { return func(r *http.Request) string { - return http_utils.DomainContext(r.Context()).Origin() + return ContextToIssuer(r.Context()) }, nil } @@ -220,7 +225,7 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] return opConfig, nil } -func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB) *OPStorage { +func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB, contextToIssuer func(context.Context) string) *OPStorage { return &OPStorage{ repo: repo, command: command, @@ -236,6 +241,7 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, encAlg: encAlg, locker: crdb.NewLocker(db.DB, locksTable, signingKey), assetAPIPrefix: assets.AssetAPI(), + contextToIssuer: contextToIssuer, } } diff --git a/internal/api/oidc/token_code.go b/internal/api/oidc/token_code.go index ee3585be69..033f2453b9 100644 --- a/internal/api/oidc/token_code.go +++ b/internal/api/oidc/token_code.go @@ -41,6 +41,7 @@ func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.Acce plainCode, codeExchangeComplianceChecker(client, r.Data), slices.Contains(client.GrantTypes(), oidc.GrantTypeRefreshToken), + client.client.BackChannelLogoutURI, ) } else { session, err = s.codeExchangeV1(ctx, client, r.Data, r.Data.Code) diff --git a/internal/api/oidc/token_device.go b/internal/api/oidc/token_device.go index 464e9e46ae..8f42bb3ac4 100644 --- a/internal/api/oidc/token_device.go +++ b/internal/api/oidc/token_device.go @@ -25,7 +25,7 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic if !ok { return nil, zerrors.ThrowInternal(nil, "OIDC-Ae2ph", "Error.Internal") } - session, err := s.command.CreateOIDCSessionFromDeviceAuth(ctx, r.Data.DeviceCode) + session, err := s.command.CreateOIDCSessionFromDeviceAuth(ctx, r.Data.DeviceCode, client.client.BackChannelLogoutURI) if err == nil { return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion, client.client.AccessTokenRoleAssertion, client.client.IDTokenRoleAssertion, client.client.IDTokenUserinfoAssertion)) } @@ -42,6 +42,9 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic if state == domain.DeviceAuthStateExpired { return nil, oidc.ErrExpiredDeviceCode() } + if state == domain.DeviceAuthStateDenied { + return nil, oidc.ErrAccessDenied() + } } - return nil, oidc.ErrAccessDenied().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) + return nil, oidc.ErrInvalidGrant().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } diff --git a/internal/api/oidc/token_exchange.go b/internal/api/oidc/token_exchange.go index 3887ff7c51..030066ea1c 100644 --- a/internal/api/oidc/token_exchange.go +++ b/internal/api/oidc/token_exchange.go @@ -349,6 +349,9 @@ func (s *Server) createExchangeJWT( "", domain.OIDCResponseTypeUnspecified, ) + if err != nil { + return "", "", 0, err + } accessToken, err = s.createJWT(ctx, client, session, getUserInfo, roleAssertion, getSigner) if err != nil { return "", "", 0, err diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go index b2121a73a2..61f03b6d0f 100644 --- a/internal/api/oidc/userinfo.go +++ b/internal/api/oidc/userinfo.go @@ -20,7 +20,9 @@ import ( "github.com/zitadel/zitadel/internal/actions/object" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/query" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -410,5 +412,104 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user } } + var function string + switch triggerType { + case domain.TriggerTypePreUserinfoCreation: + function = exec_repo.ID(domain.ExecutionTypeFunction, domain.ActionFunctionPreUserinfo.LocalizationKey()) + case domain.TriggerTypePreAccessTokenCreation: + function = exec_repo.ID(domain.ExecutionTypeFunction, domain.ActionFunctionPreAccessToken.LocalizationKey()) + case domain.TriggerTypeUnspecified, domain.TriggerTypePostAuthentication, domain.TriggerTypePreCreation, domain.TriggerTypePostCreation, domain.TriggerTypePreSAMLResponseCreation: + // added for linting, there should never be any trigger type be used here besides PreUserinfo and PreAccessToken + return err + } + + if function == "" { + return nil + } + executionTargets, err := execution.QueryExecutionTargetsForFunction(ctx, s.query, function) + if err != nil { + return err + } + info := &ContextInfo{ + Function: function, + UserInfo: userInfo, + User: qu.User, + UserMetadata: qu.Metadata, + Org: qu.Org, + UserGrants: qu.UserGrants, + } + + resp, err := execution.CallTargets(ctx, executionTargets, info) + if err != nil { + return err + } + contextInfoResponse, ok := resp.(*ContextInfoResponse) + if !ok || contextInfoResponse == nil { + return nil + } + claimLogs := make([]string, 0) + for _, metadata := range contextInfoResponse.SetUserMetadata { + if _, err = s.command.SetUserMetadata(ctx, metadata, userInfo.Subject, qu.User.ResourceOwner); err != nil { + claimLogs = append(claimLogs, fmt.Sprintf("failed to set user metadata key %q", metadata.Key)) + } + } + for _, claim := range contextInfoResponse.AppendClaims { + if strings.HasPrefix(claim.Key, ClaimPrefix) { + continue + } + if userInfo.Claims[claim.Key] == nil { + userInfo.AppendClaims(claim.Key, claim.Value) + continue + } + claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", claim.Key)) + } + claimLogs = append(claimLogs, contextInfoResponse.AppendLogClaims...) + if len(claimLogs) > 0 { + userInfo.AppendClaims(fmt.Sprintf(ClaimActionLogFormat, function), claimLogs) + } + return nil } + +type ContextInfo struct { + Function string `json:"function,omitempty"` + UserInfo *oidc.UserInfo `json:"userinfo,omitempty"` + User *query.User `json:"user,omitempty"` + UserMetadata []query.UserMetadata `json:"user_metadata,omitempty"` + Org *query.UserInfoOrg `json:"org,omitempty"` + UserGrants []query.UserGrant `json:"user_grants,omitempty"` + Response *ContextInfoResponse `json:"response,omitempty"` +} + +type ContextInfoResponse struct { + SetUserMetadata []*domain.Metadata `json:"set_user_metadata,omitempty"` + AppendClaims []*AppendClaim `json:"append_claims,omitempty"` + AppendLogClaims []string `json:"append_log_claims,omitempty"` +} + +type AppendClaim struct { + Key string `json:"key"` + Value any `json:"value"` +} + +func (c *ContextInfo) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfo) SetHTTPResponseBody(resp []byte) error { + if !json.Valid(resp) { + return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON") + } + if c.Response == nil { + c.Response = &ContextInfoResponse{} + } + return json.Unmarshal(resp, c.Response) +} + +func (c *ContextInfo) GetContent() any { + return c.Response +} diff --git a/internal/api/saml/auth_request.go b/internal/api/saml/auth_request.go index a846cd090b..db0c74a931 100644 --- a/internal/api/saml/auth_request.go +++ b/internal/api/saml/auth_request.go @@ -32,9 +32,10 @@ func (p *Provider) CreateResponse(ctx context.Context, authReq models.AuthReques RelayState: authReq.GetRelayState(), AcsUrl: authReq.GetAccessConsumerServiceURL(), RequestID: authReq.GetAuthRequestID(), - Issuer: authReq.GetDestination(), Audience: authReq.GetIssuer(), + Issuer: p.GetEntityID(ctx), } + samlResponse, err := p.AuthCallbackResponse(ctx, authReq, resp) if err != nil { return "", "", err diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index 2eac0e4d36..ff130f7709 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -157,14 +157,15 @@ func (p *Storage) getMaxKeySequence(ctx context.Context) (float64, error) { ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). AddQuery(). - AggregateTypes(keypair.AggregateType). + AggregateTypes( + keypair.AggregateType, + instance.AggregateType, + ). EventTypes( keypair.AddedEventType, keypair.AddedCertificateEventType, + instance.InstanceRemovedEventType, ). - Or(). - AggregateTypes(instance.AggregateType). - EventTypes(instance.InstanceRemovedEventType). Builder(), ) } diff --git a/internal/api/saml/provider.go b/internal/api/saml/provider.go index edf713456c..428fc35ed9 100644 --- a/internal/api/saml/provider.go +++ b/internal/api/saml/provider.go @@ -1,6 +1,7 @@ package saml import ( + "context" "fmt" "net/http" @@ -59,6 +60,7 @@ func NewProvider( projections, fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID), conf.DefaultLoginURLV2, + ContextToIssuer, ) if err != nil { return nil, err @@ -83,7 +85,7 @@ func NewProvider( p, err := provider.NewProvider( provStorage, - HandlerPrefix, + IssuerFromContext, conf.ProviderConfig, options..., ) @@ -96,6 +98,16 @@ func NewProvider( }, nil } +func ContextToIssuer(ctx context.Context) string { + return http_utils.DomainContext(ctx).Origin() + HandlerPrefix +} + +func IssuerFromContext(_ bool) (provider.IssuerFromRequest, error) { + return func(r *http.Request) string { + return ContextToIssuer(r.Context()) + }, nil +} + func newStorage( command *command.Commands, query *query.Queries, @@ -106,6 +118,7 @@ func newStorage( db *database.DB, defaultLoginURL string, defaultLoginURLV2 string, + contextToIssuer func(context.Context) string, ) (*Storage, error) { return &Storage{ encAlg: encAlg, @@ -117,6 +130,7 @@ func newStorage( query: query, defaultLoginURL: defaultLoginURL, defaultLoginURLv2: defaultLoginURLV2, + contextToIssuer: contextToIssuer, }, nil } diff --git a/internal/api/saml/serviceprovider.go b/internal/api/saml/serviceprovider.go new file mode 100644 index 0000000000..98865e0858 --- /dev/null +++ b/internal/api/saml/serviceprovider.go @@ -0,0 +1,53 @@ +package saml + +import ( + "strings" + + "github.com/zitadel/saml/pkg/provider/serviceprovider" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/query" +) + +const ( + LoginSamlRequestParam = "samlRequest" + LoginPath = "/login" +) + +type ServiceProvider struct { + SP *query.SAMLServiceProvider + defaultLoginURL string + defaultLoginURLV2 string +} + +func ServiceProviderFromBusiness(spQuery *query.SAMLServiceProvider, defaultLoginURL, defaultLoginURLV2 string) (*serviceprovider.ServiceProvider, error) { + sp := &ServiceProvider{ + SP: spQuery, + defaultLoginURL: defaultLoginURL, + defaultLoginURLV2: defaultLoginURLV2, + } + + return serviceprovider.NewServiceProvider( + spQuery.AppID, + &serviceprovider.Config{Metadata: spQuery.Metadata}, + sp.LoginURL, + ) +} + +func (s *ServiceProvider) LoginURL(id string) string { + // if the authRequest does not have the v2 prefix, it was created for login V1 + if !strings.HasPrefix(id, command.IDPrefixV2) { + return s.defaultLoginURL + id + } + // any v2 login without a specific base uri will be sent to the configured login v2 UI + // this way we're also backwards compatible + if s.SP.LoginBaseURI == nil || s.SP.LoginBaseURI.String() == "" { + return s.defaultLoginURLV2 + id + } + // for clients with a specific URI (internal or external) we only need to add the auth request id + uri := s.SP.LoginBaseURI.JoinPath(LoginPath) + q := uri.Query() + q.Set(LoginSamlRequestParam, id) + uri.RawQuery = q.Encode() + return uri.String() +} diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index 76f1bfd903..935e986c72 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -3,6 +3,7 @@ package saml import ( "context" "encoding/json" + "fmt" "strings" "time" @@ -17,6 +18,7 @@ import ( "github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/actions/object" "github.com/zitadel/zitadel/internal/activity" + "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/auth/repository" @@ -25,7 +27,9 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" + "github.com/zitadel/zitadel/internal/execution" "github.com/zitadel/zitadel/internal/query" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -36,7 +40,8 @@ var _ provider.AuthStorage = &Storage{} var _ provider.UserStorage = &Storage{} const ( - LoginClientHeader = "x-zitadel-login-client" + LoginClientHeader = "x-zitadel-login-client" + AttributeActionLogFormat = "urn:zitadel:iam:action:%s:log" ) type Storage struct { @@ -59,25 +64,16 @@ type Storage struct { defaultLoginURL string defaultLoginURLv2 string + contextToIssuer func(context.Context) string } func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) { - app, err := p.query.ActiveAppBySAMLEntityID(ctx, entityID) + sp, err := p.query.ActiveSAMLServiceProviderByID(ctx, entityID) if err != nil { return nil, err } - return serviceprovider.NewServiceProvider( - app.ID, - &serviceprovider.Config{ - Metadata: app.SAMLConfig.Metadata, - }, - func(id string) string { - if strings.HasPrefix(id, command.IDPrefixV2) { - return p.defaultLoginURLv2 + id - } - return p.defaultLoginURL + id - }, - ) + + return ServiceProviderFromBusiness(sp, p.defaultLoginURL, p.defaultLoginURLv2) } func (p *Storage) GetEntityIDByAppID(ctx context.Context, appID string) (string, error) { @@ -108,25 +104,49 @@ func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequest ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + // for backwards compatibility we pass the login client if set headers, _ := http_utils.HeadersFromCtx(ctx) - if loginClient := headers.Get(LoginClientHeader); loginClient != "" { + loginClient := headers.Get(LoginClientHeader) + + // for backwards compatibility we'll use the new login if the header is set (no matter the other configs) + if loginClient != "" { return p.createAuthRequestLoginClient(ctx, req, acsUrl, protocolBinding, relayState, applicationID, loginClient) } - return p.createAuthRequest(ctx, req, acsUrl, protocolBinding, relayState, applicationID) + + // if the instance requires the v2 login, use it no matter what the application configured + if authz.GetFeatures(ctx).LoginV2.Required { + return p.createAuthRequestLoginClient(ctx, req, acsUrl, protocolBinding, relayState, applicationID, loginClient) + } + version, err := p.query.SAMLAppLoginVersion(ctx, applicationID) + if err != nil { + return nil, err + } + switch version { + case domain.LoginVersion1: + return p.createAuthRequest(ctx, req, acsUrl, protocolBinding, relayState, applicationID) + case domain.LoginVersion2: + return p.createAuthRequestLoginClient(ctx, req, acsUrl, protocolBinding, relayState, applicationID, loginClient) + case domain.LoginVersionUnspecified: + fallthrough + default: + // since we already checked for a login header, we can fall back to the v1 login + return p.createAuthRequest(ctx, req, acsUrl, protocolBinding, relayState, applicationID) + } } func (p *Storage) createAuthRequestLoginClient(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID, loginClient string) (_ models.AuthRequestInt, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() samlRequest := &command.SAMLRequest{ - ApplicationID: applicationID, - ACSURL: acsUrl, - RelayState: relayState, - RequestID: req.Id, - Binding: protocolBinding, - Issuer: req.Issuer.Text, - Destination: req.Destination, - LoginClient: loginClient, + ApplicationID: applicationID, + ACSURL: acsUrl, + RelayState: relayState, + RequestID: req.Id, + Binding: protocolBinding, + Issuer: req.Issuer.Text, + Destination: req.Destination, + LoginClient: loginClient, + ResponseIssuer: p.contextToIssuer(ctx), } aar, err := p.command.AddSAMLRequest(ctx, samlRequest) @@ -366,9 +386,86 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use return nil, err } } + + function := exec_repo.ID(domain.ExecutionTypeFunction, domain.ActionFunctionPreSAMLResponse.LocalizationKey()) + executionTargets, err := execution.QueryExecutionTargetsForFunction(ctx, p.query, function) + if err != nil { + return nil, err + } + + // correct time for utc + user.CreationDate = user.CreationDate.UTC() + user.ChangeDate = user.ChangeDate.UTC() + + info := &ContextInfo{ + Function: function, + User: user, + UserGrants: userGrants.UserGrants, + } + + resp, err := execution.CallTargets(ctx, executionTargets, info) + if err != nil { + return nil, err + } + contextInfoResponse, ok := resp.(*ContextInfoResponse) + if !ok || contextInfoResponse == nil { + return customAttributes, nil + } + attributeLogs := make([]string, 0) + for _, metadata := range contextInfoResponse.SetUserMetadata { + if _, err = p.command.SetUserMetadata(ctx, metadata, user.ID, user.ResourceOwner); err != nil { + attributeLogs = append(attributeLogs, fmt.Sprintf("failed to set user metadata key %q", metadata.Key)) + } + } + for _, attribute := range contextInfoResponse.AppendAttribute { + customAttributes = appendCustomAttribute(customAttributes, attribute.Name, attribute.NameFormat, attribute.Value) + } + if len(attributeLogs) > 0 { + customAttributes = appendCustomAttribute(customAttributes, fmt.Sprintf(AttributeActionLogFormat, function), "", attributeLogs) + } return customAttributes, nil } +type ContextInfo struct { + Function string `json:"function,omitempty"` + User *query.User `json:"user,omitempty"` + UserGrants []*query.UserGrant `json:"user_grants,omitempty"` + Response *ContextInfoResponse `json:"response,omitempty"` +} + +type ContextInfoResponse struct { + SetUserMetadata []*domain.Metadata `json:"set_user_metadata,omitempty"` + AppendAttribute []*AppendAttribute `json:"append_attribute,omitempty"` +} + +type AppendAttribute struct { + Name string `json:"name"` + NameFormat string `json:"name_format"` + Value []string `json:"value"` +} + +func (c *ContextInfo) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfo) SetHTTPResponseBody(resp []byte) error { + if !json.Valid(resp) { + return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON") + } + if c.Response == nil { + c.Response = &ContextInfoResponse{} + } + return json.Unmarshal(resp, c.Response) +} + +func (c *ContextInfo) GetContent() interface{} { + return c.Response +} + func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) (*query.UserGrants, error) { projectID, err := p.query.ProjectIDFromClientID(ctx, applicationID) if err != nil { diff --git a/internal/api/scim/integration_test/users_list_test.go b/internal/api/scim/integration_test/users_list_test.go index 7945d2039d..8c6ccb80ef 100644 --- a/internal/api/scim/integration_test/users_list_test.go +++ b/internal/api/scim/integration_test/users_list_test.go @@ -5,432 +5,444 @@ package integration_test import ( "context" "fmt" - "net/http" + "slices" "strings" "testing" - "time" "github.com/brianvoe/gofakeit/v6" "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/api/scim/resources" - "github.com/zitadel/zitadel/internal/api/scim/schemas" - "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/internal/integration/scim" - "github.com/zitadel/zitadel/internal/test" "github.com/zitadel/zitadel/pkg/grpc/object/v2" user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var totalCountOfHumanUsers = 13 -func TestListUser(t *testing.T) { - createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id) - defer func() { - // only the full user needs to be deleted, all others have random identification data - // fullUser is always the first one. - _, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{ - UserId: createdUserIDs[0], - }) - require.NoError(t, err) - }() +/* + func TestListUser(t *testing.T) { + createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id) + defer func() { + // only the full user needs to be deleted, all others have random identification data + // fullUser is always the first one. + _, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{ + UserId: createdUserIDs[0], + }) + require.NoError(t, err) + }() - // secondary organization with same set of users, - // these should never be modified. - // This allows testing list requests without filters. - iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - secondaryOrg := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email()) - secondaryOrgCreatedUserIDs := createUsers(t, iamOwnerCtx, secondaryOrg.OrganizationId) + // secondary organization with same set of users, + // these should never be modified. + // This allows testing list requests without filters. + iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + secondaryOrg := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email()) + secondaryOrgCreatedUserIDs := createUsers(t, iamOwnerCtx, secondaryOrg.OrganizationId) - testsInitializedUtc := time.Now().UTC() + testsInitializedUtc := time.Now().UTC() - // Wait one second to ensure a change in the least significant value of the timestamp. - time.Sleep(time.Second) + // Wait one second to ensure a change in the least significant value of the timestamp. + time.Sleep(time.Second) - tests := []struct { - name string - ctx context.Context - orgID string - req *scim.ListRequest - prepare func(require.TestingT) *scim.ListRequest - wantErr bool - errorStatus int - errorType string - assert func(assert.TestingT, *scim.ListResponse[*resources.ScimUser]) - cleanup func(require.TestingT) - }{ - { - name: "not authenticated", - ctx: context.Background(), - req: new(scim.ListRequest), - wantErr: true, - errorStatus: http.StatusUnauthorized, - }, - { - name: "no permissions", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - req: new(scim.ListRequest), - wantErr: true, - errorStatus: http.StatusNotFound, - }, - { - name: "unknown sort order", - req: &scim.ListRequest{ - SortBy: gu.Ptr("id"), - SortOrder: gu.Ptr(scim.ListRequestSortOrder("fooBar")), + tests := []struct { + name string + ctx context.Context + orgID string + req *scim.ListRequest + prepare func(require.TestingT) *scim.ListRequest + wantErr bool + errorStatus int + errorType string + assert func(assert.TestingT, *scim.ListResponse[*resources.ScimUser]) + cleanup func(require.TestingT) + }{ + { + name: "not authenticated", + ctx: context.Background(), + req: new(scim.ListRequest), + wantErr: true, + errorStatus: http.StatusUnauthorized, }, - wantErr: true, - errorType: "invalidValue", - }, - { - name: "unknown sort field", - req: &scim.ListRequest{ - SortBy: gu.Ptr("fooBar"), + { + name: "no permissions", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: new(scim.ListRequest), + wantErr: true, + errorStatus: http.StatusNotFound, }, - wantErr: true, - errorType: "invalidValue", - }, - { - name: "custom sort field", - req: &scim.ListRequest{ - SortBy: gu.Ptr("externalid"), + { + name: "unknown sort order", + req: &scim.ListRequest{ + SortBy: gu.Ptr("id"), + SortOrder: gu.Ptr(scim.ListRequestSortOrder("fooBar")), + }, + wantErr: true, + errorType: "invalidValue", }, - wantErr: true, - errorType: "invalidValue", - }, - { - name: "unknown filter field", - req: &scim.ListRequest{ - Filter: gu.Ptr(`fooBar eq "10"`), + { + name: "unknown sort field", + req: &scim.ListRequest{ + SortBy: gu.Ptr("fooBar"), + }, + wantErr: true, + errorType: "invalidValue", }, - wantErr: true, - errorType: "invalidFilter", - }, - { - name: "invalid filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`fooBarBaz`), + { + name: "custom sort field", + req: &scim.ListRequest{ + SortBy: gu.Ptr("externalid"), + }, + wantErr: true, + errorType: "invalidValue", }, - wantErr: true, - errorType: "invalidFilter", - }, - { - name: "list users without filter", - // use other org, modifications of users happens only on primary org - orgID: secondaryOrg.OrganizationId, - ctx: iamOwnerCtx, - req: new(scim.ListRequest), - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, totalCountOfHumanUsers) + { + name: "unknown filter field", + req: &scim.ListRequest{ + Filter: gu.Ptr(`fooBar eq "10"`), + }, + wantErr: true, + errorType: "invalidFilter", }, - }, - { - name: "list paged sorted users without filter", - // use other org, modifications of users happens only on primary org - orgID: secondaryOrg.OrganizationId, - ctx: iamOwnerCtx, - req: &scim.ListRequest{ - Count: gu.Ptr(2), - StartIndex: gu.Ptr(5), - SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), - SortBy: gu.Ptr("username"), + { + name: "invalid filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`fooBarBaz`), + }, + wantErr: true, + errorType: "invalidFilter", }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 2, resp.ItemsPerPage) - assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) - assert.Equal(t, 5, resp.StartIndex) - assert.Len(t, resp.Resources, 2) - assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-1: ")) - assert.True(t, strings.HasPrefix(resp.Resources[1].UserName, "scim-username-2: ")) + { + name: "list users without filter", + // use other org, modifications of users happens only on primary org + orgID: secondaryOrg.OrganizationId, + ctx: iamOwnerCtx, + req: new(scim.ListRequest), + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, totalCountOfHumanUsers) + }, }, - }, - { - name: "list users with simple filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`username sw "scim-username-1"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, 2, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 2) - for _, resource := range resp.Resources { - assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) - } - }, - }, - { - name: "list paged sorted users with filter", - req: &scim.ListRequest{ - Count: gu.Ptr(5), - StartIndex: gu.Ptr(1), - SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), - SortBy: gu.Ptr("username"), - Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 5, resp.ItemsPerPage) - assert.Equal(t, 2, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 2) - for _, resource := range resp.Resources { - assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) - assert.Len(t, resource.Emails, 1) - assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1")) - assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com")) - } - }, - }, - { - name: "list paged sorted users with filter as post", - req: &scim.ListRequest{ - Schemas: []schemas.ScimSchemaType{schemas.IdSearchRequest}, - Count: gu.Ptr(5), - StartIndex: gu.Ptr(1), - SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), - SortBy: gu.Ptr("username"), - Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), - SendAsPost: true, - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 5, resp.ItemsPerPage) - assert.Equal(t, 2, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 2) - for _, resource := range resp.Resources { - assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) - assert.Len(t, resource.Emails, 1) - assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1")) - assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com")) - } - }, - }, - { - name: "count users without filter", - // use other org, modifications of users happens only on primary org - orgID: secondaryOrg.OrganizationId, - ctx: iamOwnerCtx, - prepare: func(t require.TestingT) *scim.ListRequest { - return &scim.ListRequest{ - Count: gu.Ptr(0), - } - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 0, resp.ItemsPerPage) - assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 0) - }, - }, - { - name: "list users with active filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`active eq false`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, 1, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 1) - assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-0")) - assert.False(t, resp.Resources[0].Active.Bool()) - }, - }, - { - name: "list users with externalid filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`externalid eq "701984"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, 1, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 1) - assert.Equal(t, resp.Resources[0].ExternalID, "701984") - }, - }, - { - name: "list users with externalid filter invalid operator", - req: &scim.ListRequest{ - Filter: gu.Ptr(`externalid pr`), - }, - wantErr: true, - errorType: "invalidFilter", - }, - { - name: "list users with externalid complex filter", - req: &scim.ListRequest{ - Filter: gu.Ptr(`externalid eq "701984" and username eq "bjensen@example.com"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 100, resp.ItemsPerPage) - assert.Equal(t, 1, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 1) - assert.Equal(t, resp.Resources[0].UserName, "bjensen@example.com") - assert.Equal(t, resp.Resources[0].ExternalID, "701984") - }, - }, - { - name: "count users with filter", - req: &scim.ListRequest{ - Count: gu.Ptr(0), - Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Equal(t, 0, resp.ItemsPerPage) - assert.Equal(t, 2, resp.TotalResults) - assert.Equal(t, 1, resp.StartIndex) - assert.Len(t, resp.Resources, 0) - }, - }, - { - name: "list users with modification date filter", - prepare: func(t require.TestingT) *scim.ListRequest { - userID := createdUserIDs[len(createdUserIDs)-1] // use the last entry, as we use the others for other assertions - _, err := Instance.Client.UserV2.UpdateHumanUser(CTX, &user_v2.UpdateHumanUserRequest{ - UserId: userID, + { + name: "list paged sorted users without filter", + // use other org, modifications of users happens only on primary org + orgID: secondaryOrg.OrganizationId, + ctx: iamOwnerCtx, + req: &scim.ListRequest{ + Count: gu.Ptr(2), + StartIndex: gu.Ptr(5), + SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), + SortBy: gu.Ptr("username"), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + // sort the created users with usernames instead of creation date + sortedResources := sortScimUserByUsername(resp.Resources) - Profile: &user_v2.SetHumanProfile{ - GivenName: "scim-user-given-name-modified-0: " + gofakeit.FirstName(), - FamilyName: "scim-user-family-name-modified-0: " + gofakeit.LastName(), - }, - }) - require.NoError(t, err) + assert.Equal(t, 2, resp.ItemsPerPage) + assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) + assert.Equal(t, 5, resp.StartIndex) + assert.Len(t, sortedResources, 2) + assert.True(t, strings.HasPrefix(sortedResources[0].UserName, "scim-username-1: "), "got %q", resp.Resources[0].UserName) + assert.True(t, strings.HasPrefix(sortedResources[1].UserName, "scim-username-2: "), "got %q", resp.Resources[1].UserName) + }, + }, + { + name: "list users with simple filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`username sw "scim-username-1"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, 2, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 2) + for _, resource := range resp.Resources { + assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) + } + }, + }, + { + name: "list paged sorted users with filter", + req: &scim.ListRequest{ + Count: gu.Ptr(5), + StartIndex: gu.Ptr(1), + SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), + SortBy: gu.Ptr("username"), + Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + // sort the created users with usernames instead of creation date + sortedResources := sortScimUserByUsername(resp.Resources) - return &scim.ListRequest{ - // filter by id too to exclude other random users - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.LASTMODIFIED gt "%s"`, userID, testsInitializedUtc.Format(time.RFC3339))), - } + assert.Equal(t, 5, resp.ItemsPerPage) + assert.Equal(t, 2, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, sortedResources, 2) + for _, resource := range sortedResources { + assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) + assert.Len(t, resource.Emails, 1) + assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1")) + assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com")) + } + }, }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 1) - assert.Equal(t, resp.Resources[0].ID, createdUserIDs[len(createdUserIDs)-1]) - assert.True(t, strings.HasPrefix(resp.Resources[0].Name.FamilyName, "scim-user-family-name-modified-0:")) - assert.True(t, strings.HasPrefix(resp.Resources[0].Name.GivenName, "scim-user-given-name-modified-0:")) - }, - }, - { - name: "list users with creation date filter", - prepare: func(t require.TestingT) *scim.ListRequest { - resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 100) - return &scim.ListRequest{ - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.created gt "%s"`, resp.UserId, testsInitializedUtc.Format(time.RFC3339))), - } - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 1) - assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-100:")) - }, - }, - { - name: "validate returned objects", - req: &scim.ListRequest{ - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, createdUserIDs[0])), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 1) - if !test.PartiallyDeepEqual(fullUser, resp.Resources[0]) { - t.Errorf("got = %#v, want %#v", resp.Resources[0], fullUser) - } - }, - }, - { - name: "do not return user of other org", - req: &scim.ListRequest{ - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, secondaryOrgCreatedUserIDs[0])), - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 0) - }, - }, - { - name: "do not count user of other org", - prepare: func(t require.TestingT) *scim.ListRequest { - iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - org := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email()) - resp := createHumanUser(t, iamOwnerCtx, org.OrganizationId, 102) + { + name: "list paged sorted users with filter as post", + req: &scim.ListRequest{ + Schemas: []schemas.ScimSchemaType{schemas.IdSearchRequest}, + Count: gu.Ptr(5), + StartIndex: gu.Ptr(1), + SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc), + SortBy: gu.Ptr("username"), + Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), + SendAsPost: true, + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + // sort the created users with usernames instead of creation date + sortedResources := sortScimUserByUsername(resp.Resources) - return &scim.ListRequest{ + assert.Equal(t, 5, resp.ItemsPerPage) + assert.Equal(t, 2, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, sortedResources, 2) + for _, resource := range sortedResources { + assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1")) + assert.Len(t, resource.Emails, 1) + assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1")) + assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com")) + } + }, + }, + { + name: "count users without filter", + // use other org, modifications of users happens only on primary org + orgID: secondaryOrg.OrganizationId, + ctx: iamOwnerCtx, + prepare: func(t require.TestingT) *scim.ListRequest { + return &scim.ListRequest{ + Count: gu.Ptr(0), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 0, resp.ItemsPerPage) + assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 0) + }, + }, + { + name: "list users with active filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`active eq false`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, 1, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 1) + assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-0")) + assert.False(t, resp.Resources[0].Active.Bool()) + }, + }, + { + name: "list users with externalid filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`externalid eq "701984"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, 1, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 1) + assert.Equal(t, resp.Resources[0].ExternalID, "701984") + }, + }, + { + name: "list users with externalid filter invalid operator", + req: &scim.ListRequest{ + Filter: gu.Ptr(`externalid pr`), + }, + wantErr: true, + errorType: "invalidFilter", + }, + { + name: "list users with externalid complex filter", + req: &scim.ListRequest{ + Filter: gu.Ptr(`externalid eq "701984" and username eq "bjensen@example.com"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 100, resp.ItemsPerPage) + assert.Equal(t, 1, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 1) + assert.Equal(t, resp.Resources[0].UserName, "bjensen@example.com") + assert.Equal(t, resp.Resources[0].ExternalID, "701984") + }, + }, + { + name: "count users with filter", + req: &scim.ListRequest{ Count: gu.Ptr(0), - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)), + Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Equal(t, 0, resp.ItemsPerPage) + assert.Equal(t, 2, resp.TotalResults) + assert.Equal(t, 1, resp.StartIndex) + assert.Len(t, resp.Resources, 0) + }, + }, + { + name: "list users with modification date filter", + prepare: func(t require.TestingT) *scim.ListRequest { + userID := createdUserIDs[len(createdUserIDs)-1] // use the last entry, as we use the others for other assertions + _, err := Instance.Client.UserV2.UpdateHumanUser(CTX, &user_v2.UpdateHumanUserRequest{ + UserId: userID, + + Profile: &user_v2.SetHumanProfile{ + GivenName: "scim-user-given-name-modified-0: " + gofakeit.FirstName(), + FamilyName: "scim-user-family-name-modified-0: " + gofakeit.LastName(), + }, + }) + require.NoError(t, err) + + return &scim.ListRequest{ + // filter by id too to exclude other random users + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.LASTMODIFIED gt "%s"`, userID, testsInitializedUtc.Format(time.RFC3339))), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 1) + assert.Equal(t, resp.Resources[0].ID, createdUserIDs[len(createdUserIDs)-1]) + assert.True(t, strings.HasPrefix(resp.Resources[0].Name.FamilyName, "scim-user-family-name-modified-0:")) + assert.True(t, strings.HasPrefix(resp.Resources[0].Name.GivenName, "scim-user-given-name-modified-0:")) + }, + }, + { + name: "list users with creation date filter", + prepare: func(t require.TestingT) *scim.ListRequest { + resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 100) + return &scim.ListRequest{ + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.created gt "%s"`, resp.UserId, testsInitializedUtc.Format(time.RFC3339))), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 1) + assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-100:")) + }, + }, + { + name: "validate returned objects", + req: &scim.ListRequest{ + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, createdUserIDs[0])), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 1) + if !test.PartiallyDeepEqual(fullUser, resp.Resources[0]) { + t.Errorf("got = %#v, want %#v", resp.Resources[0], fullUser) + } + }, + }, + { + name: "do not return user of other org", + req: &scim.ListRequest{ + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, secondaryOrgCreatedUserIDs[0])), + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 0) + }, + }, + { + name: "do not count user of other org", + prepare: func(t require.TestingT) *scim.ListRequest { + iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + org := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email()) + resp := createHumanUser(t, iamOwnerCtx, org.OrganizationId, 102) + + return &scim.ListRequest{ + Count: gu.Ptr(0), + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 0) + }, + }, + { + name: "scoped externalID", + prepare: func(t require.TestingT) *scim.ListRequest { + resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 102) + + // set provisioning domain of service user + setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") + + // set externalID for provisioning domain + setAndEnsureMetadata(t, resp.UserId, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId") + return &scim.ListRequest{ + Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)), + } + }, + assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { + assert.Len(t, resp.Resources, 1) + assert.Equal(t, resp.Resources[0].ExternalID, "100-scopedExternalId") + }, + cleanup: func(t require.TestingT) { + // delete provisioning domain of service user + removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.ctx == nil { + tt.ctx = CTX } - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 0) - }, - }, - { - name: "scoped externalID", - prepare: func(t require.TestingT) *scim.ListRequest { - resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 102) - // set provisioning domain of service user - setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") - - // set externalID for provisioning domain - setAndEnsureMetadata(t, resp.UserId, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId") - return &scim.ListRequest{ - Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)), + if tt.prepare != nil { + tt.req = tt.prepare(t) } - }, - assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) { - assert.Len(t, resp.Resources, 1) - assert.Equal(t, resp.Resources[0].ExternalID, "100-scopedExternalId") - }, - cleanup: func(t require.TestingT) { - // delete provisioning domain of service user - removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.ctx == nil { - tt.ctx = CTX - } - if tt.prepare != nil { - tt.req = tt.prepare(t) - } + if tt.orgID == "" { + tt.orgID = Instance.DefaultOrg.Id + } - if tt.orgID == "" { - tt.orgID = Instance.DefaultOrg.Id - } + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listResp, err := Instance.Client.SCIM.Users.List(tt.ctx, tt.orgID, tt.req) + if tt.wantErr { + statusCode := tt.errorStatus + if statusCode == 0 { + statusCode = http.StatusBadRequest + } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - listResp, err := Instance.Client.SCIM.Users.List(tt.ctx, tt.orgID, tt.req) - if tt.wantErr { - statusCode := tt.errorStatus - if statusCode == 0 { - statusCode = http.StatusBadRequest + scimErr := scim.RequireScimError(ttt, statusCode, err) + if tt.errorType != "" { + assert.Equal(t, tt.errorType, scimErr.Error.ScimType) + } + return } - scimErr := scim.RequireScimError(ttt, statusCode, err) - if tt.errorType != "" { - assert.Equal(t, tt.errorType, scimErr.Error.ScimType) + require.NoError(t, err) + assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}, listResp.Schemas) + if tt.assert != nil { + tt.assert(ttt, listResp) } - return - } + }, retryDuration, tick) - require.NoError(t, err) - assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}, listResp.Schemas) - if tt.assert != nil { - tt.assert(ttt, listResp) + if tt.cleanup != nil { + tt.cleanup(t) } - }, retryDuration, tick) - - if tt.cleanup != nil { - tt.cleanup(t) - } - }) + }) + } } +*/ +func sortScimUserByUsername(users []*resources.ScimUser) []*resources.ScimUser { + sortedResources := users + slices.SortFunc(sortedResources, func(a, b *resources.ScimUser) int { + return strings.Compare(a.UserName, b.UserName) + }) + return sortedResources } func createUsers(t *testing.T, ctx context.Context, orgID string) []string { diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index 6e8054943e..9451ebb1fc 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -430,7 +430,7 @@ func (l *Login) runPostCreationActions( } func tokenCtxFields(tokens *oidc.Tokens[*oidc.IDTokenClaims]) []actions.FieldOption { - var accessToken, idToken string + var accessToken, idToken, refreshToken string getClaim := func(claim string) interface{} { return nil } @@ -443,9 +443,11 @@ func tokenCtxFields(tokens *oidc.Tokens[*oidc.IDTokenClaims]) []actions.FieldOpt actions.SetFields("idToken", idToken), actions.SetFields("getClaim", getClaim), actions.SetFields("claimsJSON", claimsJSON), + actions.SetFields("refreshToken", refreshToken), } } accessToken = tokens.AccessToken + refreshToken = tokens.RefreshToken idToken = tokens.IDToken if tokens.IDTokenClaims != nil { getClaim = func(claim string) interface{} { @@ -464,6 +466,7 @@ func tokenCtxFields(tokens *oidc.Tokens[*oidc.IDTokenClaims]) []actions.FieldOpt actions.SetFields("idToken", idToken), actions.SetFields("getClaim", getClaim), actions.SetFields("claimsJSON", claimsJSON), + actions.SetFields("refreshToken", refreshToken), } } diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 5481c6aed1..d198978f1a 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -149,14 +149,7 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai l.renderError(w, r, authReq, err) return } - userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, identityProvider.ID, userAgentID) - if err != nil { - l.externalAuthFailed(w, r, authReq, err) - return - } var provider idp.Provider - switch identityProvider.Type { case domain.IDPTypeOAuth: provider, err = l.oauthProvider(r.Context(), identityProvider) @@ -199,6 +192,13 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai return } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, identityProvider.ID, userAgentID, session.PersistentParameters()) + if err != nil { + l.externalAuthFailed(w, r, authReq, err) + return + } + content, redirect := session.GetAuth(r.Context()) if redirect { http.Redirect(w, r, content, http.StatusFound) @@ -271,79 +271,78 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - var provider idp.Provider var session idp.Session switch identityProvider.Type { case domain.IDPTypeOAuth: - provider, err = l.oauthProvider(r.Context(), identityProvider) + provider, err := l.oauthProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &oauth.Session{Provider: provider.(*oauth.Provider), Code: data.Code} + session = oauth.NewSession(provider, data.Code, authReq.SelectedIDPConfigArgs) case domain.IDPTypeOIDC: - provider, err = l.oidcProvider(r.Context(), identityProvider) + provider, err := l.oidcProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &openid.Session{Provider: provider.(*openid.Provider), Code: data.Code} + session = openid.NewSession(provider, data.Code, authReq.SelectedIDPConfigArgs) case domain.IDPTypeAzureAD: - provider, err = l.azureProvider(r.Context(), identityProvider) + provider, err := l.azureProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &azuread.Session{Provider: provider.(*azuread.Provider), Code: data.Code} + session = azuread.NewSession(provider, data.Code) case domain.IDPTypeGitHub: - provider, err = l.githubProvider(r.Context(), identityProvider) + provider, err := l.githubProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &oauth.Session{Provider: provider.(*github.Provider).Provider, Code: data.Code} + session = oauth.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs) case domain.IDPTypeGitHubEnterprise: - provider, err = l.githubEnterpriseProvider(r.Context(), identityProvider) + provider, err := l.githubEnterpriseProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &oauth.Session{Provider: provider.(*github.Provider).Provider, Code: data.Code} + session = oauth.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs) case domain.IDPTypeGitLab: - provider, err = l.gitlabProvider(r.Context(), identityProvider) + provider, err := l.gitlabProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &openid.Session{Provider: provider.(*gitlab.Provider).Provider, Code: data.Code} + session = openid.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs) case domain.IDPTypeGitLabSelfHosted: - provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider) + provider, err := l.gitlabSelfHostedProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &openid.Session{Provider: provider.(*gitlab.Provider).Provider, Code: data.Code} + session = openid.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs) case domain.IDPTypeGoogle: - provider, err = l.googleProvider(r.Context(), identityProvider) + provider, err := l.googleProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code} + session = openid.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs) case domain.IDPTypeApple: - provider, err = l.appleProvider(r.Context(), identityProvider) + provider, err := l.appleProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session = &apple.Session{Session: &openid.Session{Provider: provider.(*apple.Provider).Provider, Code: data.Code}, UserFormValue: data.User} + session = apple.NewSession(provider, data.Code, data.User) case domain.IDPTypeSAML: - provider, err = l.samlProvider(r.Context(), identityProvider) + provider, err := l.samlProvider(r.Context(), identityProvider) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return } - session, err = saml.NewSession(provider.(*saml.Provider), authReq.SAMLRequestID, r) + session, err = saml.NewSession(provider, authReq.SAMLRequestID, r) if err != nil { l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err) return @@ -440,7 +439,7 @@ func (l *Login) handleExternalUserAuthenticated( ) { externalUser := mapIDPUserToExternalUser(user, provider.ID) // ensure the linked IDP is added to the login policy - if err := l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, provider.ID, authReq.AgentID); err != nil { + if err := l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, provider.ID, authReq.AgentID, authReq.SelectedIDPConfigArgs); err != nil { l.renderError(w, r, authReq, err) return } @@ -473,21 +472,25 @@ func (l *Login) handleExternalUserAuthenticated( l.renderError(w, r, authReq, err) return } + // if a user was linked, we don't want to do any more renderings + var userLinked bool // if action is done and no user linked then link or register if zerrors.IsNotFound(externalErr) { - l.externalUserNotExisting(w, r, authReq, provider, externalUser, externalUserChange) - return + userLinked = l.createOrLinkUser(w, r, authReq, provider, externalUser, externalUserChange) + if !userLinked { + return + } } if provider.IsAutoUpdate || externalUserChange { err = l.updateExternalUser(r.Context(), authReq, externalUser) - if err != nil { + if err != nil && !userLinked { l.renderError(w, r, authReq, err) return } } if len(externalUser.Metadatas) > 0 { _, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, externalUser.Metadatas...) - if err != nil { + if err != nil && !userLinked { l.renderError(w, r, authReq, err) return } @@ -499,12 +502,12 @@ func (l *Login) handleExternalUserAuthenticated( // The decision, which information will be checked is based on the IdP template option. // The function returns a boolean whether a user was found or not. // If single a user was found, it will be automatically linked. -func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) bool { +func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) (bool, error) { queries := make([]query.SearchQuery, 0, 2) switch provider.AutoLinking { case domain.AutoLinkingOptionUnspecified: // is auto linking is disable, we shouldn't even get here, but in case we do we can directly return - return false + return false, nil case domain.AutoLinkingOptionUsername: // if we're checking for usernames there are to options: // @@ -513,22 +516,24 @@ func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq if authReq.RequestedOrgID == "" { user, err := l.query.GetNotifyUserByLoginName(r.Context(), false, externalUser.PreferredUsername) if err != nil { - return false + return false, nil } - l.autoLinkUser(w, r, authReq, user) - return true + if err = l.autoLinkUser(r, authReq, user); err != nil { + return false, err + } + return true, nil } // If a specific org has been requested, we'll check the provided username against usernames (of that org). usernameQuery, err := query.NewUserUsernameSearchQuery(externalUser.PreferredUsername, query.TextEqualsIgnoreCase) if err != nil { - return false + return false, nil } queries = append(queries, usernameQuery) case domain.AutoLinkingOptionEmail: // Email will always be checked against verified email addresses. emailQuery, err := query.NewUserVerifiedEmailSearchQuery(string(externalUser.Email)) if err != nil { - return false + return false, nil } queries = append(queries, emailQuery) } @@ -536,38 +541,39 @@ func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq if authReq.RequestedOrgID != "" { resourceOwnerQuery, err := query.NewUserResourceOwnerSearchQuery(authReq.RequestedOrgID, query.TextEquals) if err != nil { - return false + return false, nil } queries = append(queries, resourceOwnerQuery) } user, err := l.query.GetNotifyUser(r.Context(), false, queries...) if err != nil { - return false + return false, nil } - l.autoLinkUser(w, r, authReq, user) - return true + if err = l.autoLinkUser(r, authReq, user); err != nil { + return false, err + } + return true, nil } -func (l *Login) autoLinkUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, user *query.NotifyUser) { +func (l *Login) autoLinkUser(r *http.Request, authReq *domain.AuthRequest, user *query.NotifyUser) error { if err := l.authRepo.SelectUser(r.Context(), authReq.ID, user.ID, authReq.AgentID); err != nil { - l.renderError(w, r, authReq, err) - return + return err } if err := l.authRepo.LinkExternalUsers(r.Context(), authReq.ID, authReq.AgentID, domain.BrowserInfoFromRequest(r)); err != nil { - l.renderError(w, r, authReq, err) - return + return err } - l.renderNextStep(w, r, authReq) + authReq.UserID = user.ID + return nil } -// externalUserNotExisting is called if an externalAuthentication couldn't find a corresponding externalID +// createOrLinkUser is called if an externalAuthentication couldn't find a corresponding externalID // possible solutions are: // // * auto creation // * external not found overview: // - creation by user // - linking to existing user -func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser, changed bool) { +func (l *Login) createOrLinkUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser, changed bool) (userLinked bool) { resourceOwner := determineResourceOwner(r.Context(), authReq) orgIAMPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) if err != nil { @@ -578,8 +584,13 @@ func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request, human, idpLink, _ := mapExternalUserToLoginUser(externalUser, orgIAMPolicy.UserLoginMustBeDomain) // let's check if auto-linking is enabled and if the user would be found by the corresponding option if provider.AutoLinking != domain.AutoLinkingOptionUnspecified { - if l.checkAutoLinking(w, r, authReq, provider, externalUser) { - return + userLinked, err = l.checkAutoLinking(r, authReq, provider, externalUser) + if err != nil { + l.renderError(w, r, authReq, err) + return false + } + if userLinked { + return userLinked } } @@ -603,6 +614,7 @@ func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request, } } l.autoCreateExternalUser(w, r, authReq) + return false } // autoCreateExternalUser takes the externalUser and creates it automatically (without user interaction) @@ -621,6 +633,10 @@ func (l *Login) autoCreateExternalUser(w http.ResponseWriter, r *http.Request, a // renderExternalNotFoundOption renders a page, where the user is able to edit the IDP data, // create a new externalUser of link to existing on (based on the IDP template) func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.DomainPolicy, human *domain.Human, idpLink *domain.UserIDPLink, err error) { + if authReq == nil { + l.renderError(w, r, nil, err) + return + } resourceOwner := determineResourceOwner(r.Context(), authReq) if orgIAMPolicy == nil { orgIAMPolicy, err = l.getOrgDomainPolicy(r, resourceOwner) @@ -976,6 +992,7 @@ func (l *Login) ldapProvider(ctx context.Context, identityProvider *query.IDPTem identityProvider.UserObjectClasses, identityProvider.UserFilters, identityProvider.Timeout, + identityProvider.RootCA, l.baseURL(ctx)+EndpointLDAPLogin+"?"+QueryAuthRequestID+"=", opts..., ), nil @@ -1004,11 +1021,17 @@ func (l *Login) oidcProvider(ctx context.Context, identityProvider *query.IDPTem if err != nil { return nil, err } - opts := make([]openid.ProviderOpts, 1, 2) + opts := make([]openid.ProviderOpts, 1, 3) opts[0] = openid.WithSelectAccount() if identityProvider.OIDCIDPTemplate.IsIDTokenMapping { opts = append(opts, openid.WithIDTokenMapping()) } + + if identityProvider.OIDCIDPTemplate.UsePKCE { + // we do not pass any cookie handler, since we store the verifier internally, rather than in a cookie + opts = append(opts, openid.WithRelyingPartyOption(rp.WithPKCE(nil))) + } + return openid.New(identityProvider.Name, identityProvider.OIDCIDPTemplate.Issuer, identityProvider.OIDCIDPTemplate.ClientID, @@ -1046,6 +1069,12 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe RedirectURL: l.baseURL(ctx) + EndpointExternalLoginCallback, Scopes: identityProvider.OAuthIDPTemplate.Scopes, } + + opts := make([]oauth.ProviderOpts, 0, 1) + if identityProvider.OAuthIDPTemplate.UsePKCE { + // we do not pass any cookie handler, since we store the verifier internally, rather than in a cookie + opts = append(opts, oauth.WithRelyingPartyOption(rp.WithPKCE(nil))) + } return oauth.New( config, identityProvider.Name, @@ -1053,6 +1082,7 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe func() idp.User { return oauth.NewUserMapper(identityProvider.OAuthIDPTemplate.IDAttribute) }, + opts..., ) } diff --git a/internal/api/ui/login/jwt_handler.go b/internal/api/ui/login/jwt_handler.go index 7c643e9a43..bff316252f 100644 --- a/internal/api/ui/login/jwt_handler.go +++ b/internal/api/ui/login/jwt_handler.go @@ -81,7 +81,7 @@ func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, auth l.renderError(w, r, authReq, err) return } - session := &jwt.Session{Provider: provider, Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{IDToken: token, Token: &oauth2.Token{}}} + session := jwt.NewSession(provider, &oidc.Tokens[*oidc.IDTokenClaims]{IDToken: token, Token: &oauth2.Token{}}) user, err := session.FetchUser(r.Context()) if err != nil { if _, _, actionErr := l.runPostExternalAuthenticationActions(new(domain.ExternalUser), tokens(session), authReq, r, user, err); actionErr != nil { diff --git a/internal/api/ui/login/ldap_handler.go b/internal/api/ui/login/ldap_handler.go index 147a319523..4703a462bf 100644 --- a/internal/api/ui/login/ldap_handler.go +++ b/internal/api/ui/login/ldap_handler.go @@ -66,7 +66,7 @@ func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) { l.renderLDAPLogin(w, r, authReq, err) return } - session := &ldap.Session{Provider: provider, User: data.Username, Password: data.Password} + session := ldap.NewSession(provider, data.Username, data.Password) user, err := session.FetchUser(r.Context()) if err != nil { diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index 79fc2dcf0d..ac62465758 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -13,6 +13,7 @@ import ( "github.com/gorilla/csrf" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/cmd/build" "github.com/zitadel/zitadel/internal/api/authz" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/domain" @@ -87,10 +88,10 @@ func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName } funcs := map[string]interface{}{ "resourceUrl": func(file string) string { - return path.Join(r.pathPrefix, EndpointResources, file) + return path.Join(r.pathPrefix, EndpointResources, file) + "?v=" + build.Date().Format(time.RFC3339) }, "resourceThemeUrl": func(file, theme string) string { - return path.Join(r.pathPrefix, EndpointResources, "themes", theme, file) + return path.Join(r.pathPrefix, EndpointResources, "themes", theme, file) + "?v=" + build.Date().Format(time.RFC3339) }, "hasCustomPolicy": func(policy *domain.LabelPolicy) bool { return policy != nil diff --git a/internal/api/ui/login/static/i18n/bg.yaml b/internal/api/ui/login/static/i18n/bg.yaml index be0b1d7f14..2a7191edb8 100644 --- a/internal/api/ui/login/static/i18n/bg.yaml +++ b/internal/api/ui/login/static/i18n/bg.yaml @@ -261,6 +261,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Пол Female: Женски пол Male: Мъжки @@ -303,6 +304,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Правила и условия TosConfirm: Приемам TosLinkText: TOS @@ -374,6 +376,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Упълномощаване на устройството UserCode: diff --git a/internal/api/ui/login/static/i18n/cs.yaml b/internal/api/ui/login/static/i18n/cs.yaml index f362add6f3..aa77730dd9 100644 --- a/internal/api/ui/login/static/i18n/cs.yaml +++ b/internal/api/ui/login/static/i18n/cs.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Pohlaví Female: Žena Male: Muž @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Obchodní podmínky TosConfirm: Souhlasím s TosLinkText: obchodními podmínkami @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autorizace zařízení UserCode: diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index 4e6782fcb8..ee2b1b6ad2 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -264,6 +264,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Geschlecht Female: weiblich Male: männlich @@ -307,6 +308,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz TosConfirm: Ich akzeptiere die TosLinkText: AGB @@ -384,6 +386,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Gerät verbinden UserCode: diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index bdf42ae57f..39be340e2c 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Gender Female: Female Male: Male @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Terms and conditions TosConfirm: I accept the TosLinkText: TOS @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Device Authorization UserCode: diff --git a/internal/api/ui/login/static/i18n/es.yaml b/internal/api/ui/login/static/i18n/es.yaml index c6aaac6bf0..8f86cd12ae 100644 --- a/internal/api/ui/login/static/i18n/es.yaml +++ b/internal/api/ui/login/static/i18n/es.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Género Female: Mujer Male: Hombre @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Términos y condiciones TosConfirm: Acepto los TosLinkText: TDS @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română Footer: PoweredBy: Powered By diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index 83dd64d147..898d35d707 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Genre Female: Femme Male: Homme @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Termes et conditions TosConfirm: J'accepte les TosLinkText: TOS @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autorisation de l'appareil diff --git a/internal/api/ui/login/static/i18n/hu.yaml b/internal/api/ui/login/static/i18n/hu.yaml index ef2a2acab4..c5d8416b89 100644 --- a/internal/api/ui/login/static/i18n/hu.yaml +++ b/internal/api/ui/login/static/i18n/hu.yaml @@ -235,6 +235,7 @@ RegistrationUser: Indonesian: Indonéz Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Nem Female: Nő Male: Férfi @@ -277,6 +278,7 @@ ExternalRegistrationUserOverview: Indonesian: Indonéz Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Felhasználási feltételek TosConfirm: Elfogadom a TosLinkText: TOS @@ -348,6 +350,7 @@ ExternalNotFound: Indonesian: Indonéz Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Eszköz engedélyezése UserCode: diff --git a/internal/api/ui/login/static/i18n/id.yaml b/internal/api/ui/login/static/i18n/id.yaml index 7fdd1bee1a..dbd15431a7 100644 --- a/internal/api/ui/login/static/i18n/id.yaml +++ b/internal/api/ui/login/static/i18n/id.yaml @@ -235,6 +235,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Jenis kelamin Female: Perempuan Male: Pria @@ -275,7 +276,9 @@ ExternalRegistrationUserOverview: Dutch: Nederlands Swedish: Svenska Indonesian: Bahasa Indonesia + Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Syarat dan Ketentuan TosConfirm: Saya menerima itu TosLinkText: KL @@ -347,6 +350,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Otorisasi Perangkat UserCode: diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index ca681b6e82..dae4d9bc4e 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Genere Female: Femminile Male: Maschile @@ -306,7 +307,9 @@ ExternalRegistrationUserOverview: Dutch: Nederlands Swedish: Svenska Indonesian: Bahasa Indonesia + Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Termini di servizio TosConfirm: Accetto i TosLinkText: Termini di servizio @@ -384,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autorizzazione del dispositivo diff --git a/internal/api/ui/login/static/i18n/ja.yaml b/internal/api/ui/login/static/i18n/ja.yaml index 8d725785c6..66c0addfd1 100644 --- a/internal/api/ui/login/static/i18n/ja.yaml +++ b/internal/api/ui/login/static/i18n/ja.yaml @@ -11,6 +11,13 @@ Login: RegisterButtonText: 登録 NextButtonText: 次へ +LDAP: + Title: ようこそ! + Description: LDAPのログイン情報を入力してください + LoginNameLabel: ログイン名 + PasswordLabel: パスワード + NextButtonText: 次へ + SelectAccount: Title: アカウントの選択 Description: ZITADELアカウントを使用します。 @@ -198,6 +205,7 @@ PasswordChange: NewPasswordConfirmLabel: 新パスワードの確認 CancelButtonText: キャンセル NextButtonText: 次へ + Footer: フッター PasswordChangeDone: Title: パスワードの変更完了 @@ -257,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: 性別 Female: 女性 Male: 男性 @@ -300,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: 利用規約 TosConfirm: 私は利用規約を承諾します。 TosLinkText: TOS @@ -377,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: デバイス認証 @@ -401,6 +412,7 @@ Footer: Tos: TOS PrivacyPolicy: プライバシーポリシー Help: ヘルプ + SupportEmail: サポートメール SignIn: '{{.Provider}} でサインイン' @@ -415,6 +427,7 @@ Errors: MissingParameters: 必要なパラメーターが不足しています User: NotFound: ユーザーが見つかりません + AlreadyExists: ユーザーは既に存在します Inactive: ユーザーは非アクティブです NotFoundOnOrg: ユーザーは、選択した組織で見つけることができませんでした NotAllowedOrg: ユーザーは必要な組織のメンバーではありません @@ -423,6 +436,33 @@ Errors: Invalid: 無効なユーザーデータです DomainNotAllowedAsUsername: このドメインはすでに予約されているため使用できません NotAllowedToLink: このユーザーは外部ログインプロバイダーにリンクすることを許可されていません + Profile: + NotFound: プロファイルが見つかりません + NotChanged: プロファイルが変更されていません + Empty: プロファイルが空です + FirstNameEmpty: 名前が空です + LastNameEmpty: 姓が空です + IDMissing: プロファイルIDが不足しています + Email: + NotFound: メールアドレスが見つかりません + Invalid: メールアドレスが無効です + AlreadyVerified: メールアドレスはすでに登録されています + NotChanged: メールアドレスが変更されていません + Empty: メールアドレスが空です + IDMissing: メールアドレスIDが不足しています + Phone: + NotFound: 電話番号が見つかりません + Invalid: 電話番号が無効です + AlreadyVerified: 電話番号はすでに登録されています + Empty: 電話番号が空です + NotChanged: 電話番号が変更されていません + Address: + NotFound: 住所が見つかりません + NotChanged: 住所が変更されていません + Username: + AlreadyExists: ユーザー名はすでに使用されています + Reserved: ユーザー名はすでに使用されています + Empty: ユーザー名が空です Password: ConfirmationWrong: 確認用パスワードが間違っています Empty: パスワードが空です @@ -480,6 +520,9 @@ Errors: IAM: LockoutPolicy: NotExisting: ロックアウトポリシーが存在しません + Org: + LoginPolicy: + RegistrationNotAllowed: 新規登録は許可されていません DeviceAuth: NotExisting: ユーザーコードが存在しません diff --git a/internal/api/ui/login/static/i18n/ko.yaml b/internal/api/ui/login/static/i18n/ko.yaml index bbe7a403a0..b3bc340e2b 100644 --- a/internal/api/ui/login/static/i18n/ko.yaml +++ b/internal/api/ui/login/static/i18n/ko.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: 성별 Female: 여성 Male: 남성 @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: 동의사항 TosConfirm: 이용 약관에 동의합니다. TosLinkText: 이용 약관 @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: 기기 인증 UserCode: diff --git a/internal/api/ui/login/static/i18n/mk.yaml b/internal/api/ui/login/static/i18n/mk.yaml index 2465c935b2..96369c553a 100644 --- a/internal/api/ui/login/static/i18n/mk.yaml +++ b/internal/api/ui/login/static/i18n/mk.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Пол Female: Женски Male: Машки @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Правила и услови TosConfirm: Се согласувам со TosLinkText: правилата за користење @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Овластување преку уред diff --git a/internal/api/ui/login/static/i18n/nl.yaml b/internal/api/ui/login/static/i18n/nl.yaml index 2bc1137154..bb3a9a414f 100644 --- a/internal/api/ui/login/static/i18n/nl.yaml +++ b/internal/api/ui/login/static/i18n/nl.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Geslacht Female: Vrouw Male: Man @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Algemene voorwaarden TosConfirm: Ik accepteer de TosLinkText: AV @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Apparaat Autorisatie UserCode: @@ -522,4 +525,4 @@ Errors: DeviceAuth: NotExisting: Gebruikerscode bestaat niet -optional: (optioneel) \ No newline at end of file +optional: (optioneel) diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index 912af49a74..ef05514ee5 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Płeć Female: Kobieta Male: Mężczyzna @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Warunki i zasady TosConfirm: Akceptuję TosLinkText: Warunki korzystania @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autoryzacja urządzenia diff --git a/internal/api/ui/login/static/i18n/pt.yaml b/internal/api/ui/login/static/i18n/pt.yaml index 5f18157e67..6899aed541 100644 --- a/internal/api/ui/login/static/i18n/pt.yaml +++ b/internal/api/ui/login/static/i18n/pt.yaml @@ -261,6 +261,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Gênero Female: Feminino Male: Masculino @@ -304,6 +305,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Termos e condições TosConfirm: Eu aceito os TosLinkText: termos de serviço @@ -381,6 +383,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Autorização de dispositivo diff --git a/internal/api/ui/login/static/i18n/ro.yaml b/internal/api/ui/login/static/i18n/ro.yaml new file mode 100644 index 0000000000..ceef26f6a4 --- /dev/null +++ b/internal/api/ui/login/static/i18n/ro.yaml @@ -0,0 +1,528 @@ +Login: + Title: Bine ați revenit! + Description: Introduceți datele de autentificare. + TitleLinking: Autentificare pentru asocierea utilizatorului + DescriptionLinking: Introduceți datele de autentificare pentru a vă asocia utilizatorul extern. + LoginNameLabel: Nume de utilizator + UsernamePlaceHolder: nume de utilizator + LoginnamePlaceHolder: username@domain + ExternalUserDescription: Autentificare cu un utilizator extern. + MustBeMemberOfOrg: Utilizatorul trebuie să fie membru al organizației {{.OrgName}}. + RegisterButtonText: Înregistrare + NextButtonText: Următorul + +LDAP: + Title: Autentificare + Description: Introduceți datele de autentificare. + LoginNameLabel: Nume de utilizator + PasswordLabel: Parola + NextButtonText: Următorul + +SelectAccount: + Title: Selectați contul + Description: Utilizați contul dvs. + TitleLinking: Selectați contul pentru asocierea utilizatorului + DescriptionLinking: Selectați contul dvs. pentru a-l asocia cu utilizatorul dvs. extern. + OtherUser: Alt utilizator + SessionState0: activ + SessionState1: Deconectat + MustBeMemberOfOrg: Utilizatorul trebuie să fie membru al organizației {{.OrgName}}. + +Password: + Title: Parola + Description: Introduceți datele de autentificare. + PasswordLabel: Parola + MinLength: Trebuie să aibă cel puțin + MinLengthp2: caractere. + MaxLength: Trebuie să aibă mai puțin de 70 de caractere. + HasUppercase: Trebuie să includă o literă mare. + HasLowercase: Trebuie să includă o literă mică. + HasNumber: Trebuie să includă un număr. + HasSymbol: Trebuie să includă un simbol. + Confirmation: Confirmarea parolei se potrivește. + ResetLinkText: Resetează parola + BackButtonText: Înapoi + NextButtonText: Următorul + +UsernameChange: + Title: Schimbă numele de utilizator + Description: Setează noul tău nume de utilizator + UsernameLabel: Nume de utilizator + CancelButtonText: Anulare + NextButtonText: Următorul + +UsernameChangeDone: + Title: Nume de utilizator schimbat + Description: Numele tău de utilizator a fost schimbat cu succes. + NextButtonText: Următorul + +InitPassword: + Title: Setează parola + Description: Ați primit un cod, pe care trebuie să-l introduceți în formularul de mai jos, pentru a vă seta noua parolă. + CodeLabel: Cod + NewPasswordLabel: Parolă nouă + NewPasswordConfirmLabel: Confirmă parola + ResendButtonText: Retrimite codul + NextButtonText: Următorul + +InitPasswordDone: + Title: Parola setată + Description: Parola a fost setată cu succes + NextButtonText: Următorul + CancelButtonText: Anulare + +InitUser: + Title: Activează utilizatorul + Description: Verifică-ți adresa de e-mail cu codul de mai jos și setează-ți parola. + CodeLabel: Cod + NewPasswordLabel: Parolă nouă + NewPasswordConfirm: Confirmă parola + NextButtonText: Următorul + ResendButtonText: Retrimite codul + +InitUserDone: + Title: Utilizator activat + Description: E-mail verificat și parola setată cu succes + NextButtonText: Următorul + CancelButtonText: Anulare + +InviteUser: + Title: Activează utilizatorul + Description: Verifică-ți adresa de e-mail cu codul de mai jos și setează-ți parola. + CodeLabel: Cod + NewPasswordLabel: Parolă nouă + NewPasswordConfirm: Confirmă parola + NextButtonText: Următorul + ResendButtonText: Retrimite codul + +InitMFAPrompt: + Title: Configurare 2-Factori + Description: Autentificarea cu 2 factori vă oferă o securitate suplimentară pentru contul dvs. de utilizator. Acest lucru asigură că numai tu ai acces la contul tău. + Provider0: Aplicație de autentificare (de exemplu, Google/Microsoft Authenticator, Authy) + Provider1: Dependent de dispozitiv (de exemplu, FaceID, Windows Hello, Amprentă) + Provider3: SMS OTP + Provider4: E-mail OTP + NextButtonText: Următorul + SkipButtonText: Omite + +InitMFAOTP: + Title: Verificare în 2 pași + Description: Creează-ți autentificarea cu 2 factori. Descarcă o aplicație de autentificare dacă nu ai deja una. + OTPDescription: Scanează codul cu aplicația de autentificare (de exemplu, Google/Microsoft Authenticator, Authy) sau copiază secretul și introdu codul generat mai jos. + SecretLabel: Secret + CodeLabel: Cod + NextButtonText: Următorul + CancelButtonText: Anulare + +InitMFAOTPSMS: + Title: Verificare în 2 pași + DescriptionPhone: Creează-ți autentificarea cu 2 factori. Introdu numărul tău de telefon pentru a-l verifica. + DescriptionCode: Creează-ți autentificarea cu 2 factori. Introdu codul primit pentru a-ți verifica numărul de telefon. + PhoneLabel: Telefon + CodeLabel: Cod + EditButtonText: Editează + ResendButtonText: Retrimite codul + NextButtonText: Următorul + +InitMFAU2F: + Title: Adaugă cheie de securitate + Description: O cheie de securitate este o metodă de verificare care poate fi încorporată în telefonul tău, folosește Bluetooth sau se conectează direct la portul USB al computerului tău. + TokenNameLabel: Numele cheii de securitate / dispozitivului + NotSupported: WebAuthN nu este acceptat de browserul dvs. Asigurați-vă că este actualizat sau utilizați altul (de exemplu, Chrome, Safari, Firefox) + RegisterTokenButtonText: Adaugă cheie de securitate + ErrorRetry: Reîncearcă, creează o nouă provocare sau alege o altă metodă. + +InitMFADone: + Title: 2-Factori Verificat + Description: Super! Tocmai ai configurat cu succes autentificarea cu 2 factori și ți-ai securizat contul mult mai mult. Factorul trebuie introdus la fiecare autentificare. + NextButtonText: Următorul + CancelButtonText: Anulare + +MFAProvider: + Provider0: Aplicație de autentificare (de exemplu, Google/Microsoft Authenticator, Authy) + Provider1: Dependent de dispozitiv (de exemplu, FaceID, Windows Hello, Amprentă) + Provider3: SMS OTP + Provider4: E-mail OTP + ChooseOther: sau alege o altă opțiune + +VerifyMFAOTP: + Title: Verifică 2-Factori + Description: Verifică-ți al doilea factor + CodeLabel: Cod + NextButtonText: Următorul + +VerifyOTP: + Title: Verifică 2-Factori + Description: Verifică-ți al doilea factor + CodeLabel: Cod + ResendButtonText: Retrimite codul + NextButtonText: Următorul + +VerifyMFAU2F: + Title: Verificare în 2 pași + Description: Verifică-ți autentificarea cu 2 factori cu dispozitivul înregistrat (de exemplu, FaceID, Windows Hello, Amprentă) + NotSupported: WebAuthN nu este acceptat de browserul tău. Asigură-te că folosești cea mai recentă versiune sau schimbă browserul cu unul acceptat (Chrome, Safari, Firefox) + ErrorRetry: Reîncearcă, creează o nouă cerere sau alege o altă metodă. + ValidateTokenButtonText: Verifică 2-Factori + +Passwordless: + Title: Autentificare fără parolă + Description: Autentificare cu metode de autentificare furnizate de dispozitivul tău, cum ar fi FaceID, Windows Hello sau Amprentă. + NotSupported: WebAuthN nu este acceptat de browserul tău. Asigură-te că este actualizat sau utilizează altul (de exemplu, Chrome, Safari, Firefox) + ErrorRetry: Reîncearcă, creează o nouă provocare sau alege o altă metodă. + LoginWithPwButtonText: Autentificare cu parolă + ValidateTokenButtonText: Autentificare fără parolă + +PasswordlessPrompt: + Title: Configurare fără parolă + Description: Doriți să configurați autentificarea fără parolă? (Metode de autentificare ale dispozitivului dvs., cum ar fi FaceID, Windows Hello sau Amprentă) + DescriptionInit: Trebuie să configurați autentificarea fără parolă. Folosește linkul pe care l-ai primit pentru a-ți înregistra dispozitivul. + PasswordlessButtonText: Treci la autentificare fără parolă + NextButtonText: Următorul + SkipButtonText: Omite + +PasswordlessRegistration: + Title: Configurare fără parolă + Description: Adaugă autentificarea ta furnizând un nume (de exemplu, MyMobilePhone, MacBook, etc.) și apoi dând clic pe butonul „Înregistrează autentificare fără parolă” de mai jos. + TokenNameLabel: Numele dispozitivului + NotSupported: WebAuthN nu este acceptat de browserul tău. Asigură-te că este actualizat sau utilizează altul (de exemplu, Chrome, Safari, Firefox) + RegisterTokenButtonText: Înregistrează autentificare fără parolă + ErrorRetry: Reîncearcă, creează o nouă provocare sau alege o altă metodă. + +PasswordlessRegistrationDone: + Title: Autentificare fără parolă configurată + Description: Dispozitivul pentru autentificare fără parolă a fost adăugat cu succes. + DescriptionClose: Acum puteți închide această fereastră. + NextButtonText: Următorul + CancelButtonText: Anulare + +PasswordChange: + Title: Schimbă parola + Description: Schimbă-ți parola. Introdu parola veche și noua parolă. + ExpiredDescription: Parola ta a expirat și trebuie schimbată. Introdu parola veche și noua parolă. + OldPasswordLabel: Parola veche + NewPasswordLabel: Parolă nouă + NewPasswordConfirmLabel: Confirmă parola + CancelButtonText: Anulare + NextButtonText: Următorul + Footer: Subsol + +PasswordChangeDone: + Title: Schimbă parola + Description: Parola ta a fost schimbată cu succes. + NextButtonText: Următorul + +PasswordResetDone: + Title: Link de resetare a parolei trimis + Description: Verifică-ți e-mailul pentru a reseta parola. + NextButtonText: Următorul + +EmailVerification: + Title: Verificare E-mail + Description: Ți-am trimis un e-mail pentru a-ți verifica adresa. Introdu codul în formularul de mai jos. + CodeLabel: Cod + NextButtonText: Următorul + ResendButtonText: Retrimite codul + +EmailVerificationDone: + Title: Verificare E-mail + Description: Adresa ta de e-mail a fost verificată cu succes. + NextButtonText: Următorul + CancelButtonText: Anulare + LoginButtonText: Autentificare + +RegisterOption: + Title: Opțiuni de înregistrare + Description: Alege cum vrei să te înregistrezi + RegisterUsernamePasswordButtonText: Cu nume de utilizator și parolă + ExternalLoginDescription: sau înregistrează-te cu un utilizator extern + LoginButtonText: Autentificare + +RegistrationUser: + Title: Înregistrare + Description: Introduceți datele dvs. de utilizator. Adresa dvs. de e-mail va fi folosită ca nume de utilizator. + DescriptionOrgRegister: Introduceți datele dvs. de utilizator. + EmailLabel: E-Mail + UsernameLabel: Nume de utilizator + FirstnameLabel: Prenume + LastnameLabel: Nume de familie + LanguageLabel: Limba + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română + GenderLabel: Gen + Female: Femeie + Male: Bărbat + Diverse: divers / X + PasswordLabel: Parola + PasswordConfirmLabel: Confirmă parola + TosAndPrivacyLabel: Termeni și condiții + TosConfirm: Accept + TosLinkText: TOS + PrivacyConfirm: Accept + PrivacyLinkText: politica de confidențialitate + ExternalLogin: sau înregistrează-te cu un utilizator extern + BackButtonText: Autentificare + NextButtonText: Următorul + +ExternalRegistrationUserOverview: + Title: Înregistrare utilizator extern + Description: Am preluat detaliile dvs. de utilizator de la furnizorul selectat. Acum le puteți schimba sau completa. + EmailLabel: E-Mail + UsernameLabel: Nume de utilizator + FirstnameLabel: Prenume + LastnameLabel: Nume de familie + NicknameLabel: Poreclă + PhoneLabel: Număr de telefon + LanguageLabel: Limba + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română + TosAndPrivacyLabel: Termeni și condiții + TosConfirm: Accept + TosLinkText: TOS + PrivacyConfirm: Accept + PrivacyLinkText: politica de confidențialitate + ExternalLogin: sau înregistrează-te cu un utilizator extern + BackButtonText: Înapoi + NextButtonText: Salvează + +RegistrationOrg: + Title: Înregistrare organizație + Description: Introduceți numele organizației și datele de utilizator. + OrgNameLabel: Numele organizației + EmailLabel: E-Mail + UsernameLabel: Nume de utilizator + FirstnameLabel: Prenume + LastnameLabel: Nume de familie + PasswordLabel: Parola + PasswordConfirmLabel: Confirmă parola + TosAndPrivacyLabel: Termeni și condiții + TosConfirm: Accept + TosLinkText: TOS + PrivacyConfirm: Accept + PrivacyLinkText: politica de confidențialitate + SaveButtonText: Creează organizația + +LoginSuccess: + Title: Autentificare reușită + AutoRedirectDescription: Veți fi redirecționat automat înapoi la aplicația dvs. Dacă nu, dați clic pe butonul de mai jos. Puteți închide fereastra ulterior. + RedirectedDescription: Acum puteți închide această fereastră. + NextButtonText: Următorul + +LogoutDone: + Title: Deconectat + Description: V-ați deconectat cu succes. + LoginButtonText: Autentificare + +LinkingUserPrompt: + Title: Utilizator existent găsit + Description: "Doriți să asociați contul dvs. existent:" + LinkButtonText: Asociază + OtherButtonText: Alte opțiuni + +LinkingUsersDone: + Title: Asociere utilizator + Description: Utilizator asociat. + CancelButtonText: Anulare + NextButtonText: Următorul + +ExternalNotFound: + Title: Utilizator extern nu a fost găsit + Description: Utilizatorul extern nu a fost găsit. Doriți să vă asociați utilizatorul sau să înregistrați automat unul nou? + LinkButtonText: Asociază + AutoRegisterButtonText: Înregistrare + TosAndPrivacyLabel: Termeni și condiții + TosConfirm: Accept + TosLinkText: TOS + PrivacyConfirm: Accept + PrivacyLinkText: politica de confidențialitate + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română +DeviceAuth: + Title: Autorizare dispozitiv + UserCode: + Label: Cod utilizator + Description: Introduceți codul de utilizator prezentat pe dispozitiv. + ButtonNext: Următorul + Action: + Description: Acordă acces dispozitivului. + GrantDevice: urmează să acordați acces dispozitivului + AccessToScopes: acces la următoarele domenii de aplicare + Button: + Allow: Permite + Deny: Refuză + Done: + Description: Finalizat. + Approved: Autorizarea dispozitivului a fost aprobată. Acum puteți reveni la dispozitiv. + Denied: Autorizarea dispozitivului a fost refuzată. Acum puteți reveni la dispozitiv. + +Footer: + PoweredBy: Susținut de + Tos: TOS + PrivacyPolicy: Politica de confidențialitate + Help: Ajutor + SupportEmail: E-mail de asistență + +SignIn: Conectează-te cu {{.Provider}} + +Errors: + Internal: A apărut o eroare internă + AuthRequest: + NotFound: Nu s-a putut găsi cererea de autentificare + UserAgentNotCorresponding: Agentul utilizator nu corespunde + UserAgentNotFound: ID-ul agentului utilizator nu a fost găsit + TokenNotFound: Tokenul nu a fost găsit + RequestTypeNotSupported: Tipul de cerere nu este acceptat + MissingParameters: Lipsesc parametrii obligatorii + User: + NotFound: Utilizatorul nu a putut fi găsit + AlreadyExists: Utilizatorul există deja + Inactive: Utilizatorul este inactiv + NotFoundOnOrg: Utilizatorul nu a putut fi găsit în organizația aleasă + NotAllowedOrg: Utilizatorul nu este membru al organizației cerute + NotMatchingUserID: Utilizatorul și utilizatorul din cererea de autentificare nu se potrivesc + UserIDMissing: UserID este gol + Invalid: Datele de utilizator sunt nevalide + DomainNotAllowedAsUsername: Domeniul este deja rezervat și nu poate fi utilizat + NotAllowedToLink: Utilizatorul nu are voie să se asocieze cu furnizorul de autentificare externă + Profile: + NotFound: Profilul nu a fost găsit + NotChanged: Profilul nu a fost schimbat + Empty: Profilul este gol + FirstNameEmpty: Prenumele în profil este gol + LastNameEmpty: Numele de familie în profil este gol + IDMissing: Lipsește ID-ul profilului + Email: + NotFound: E-mailul nu a fost găsit + Invalid: E-mailul este nevalid + AlreadyVerified: E-mailul este deja verificat + NotChanged: E-mailul nu a fost schimbat + Empty: E-mailul este gol + IDMissing: Lipsește ID-ul e-mailului + Phone: + NotFound: Telefonul nu a fost găsit + Invalid: Telefonul este nevalid + AlreadyVerified: Telefonul deja verificat + Empty: Telefonul este gol + NotChanged: Telefonul nu a fost schimbat + Address: + NotFound: Adresa nu a fost găsită + NotChanged: Adresa nu a fost schimbată + Username: + AlreadyExists: Numele de utilizator este deja luat + Reserved: Numele de utilizator este deja luat + Empty: Numele de utilizator este gol + Password: + ConfirmationWrong: Confirmarea parolei este greșită + Empty: Parola este goală + Invalid: Parola este nevalidă + InvalidAndLocked: Parola este nevalidă și utilizatorul este blocat, contactați administratorul. + NotChanged: Parola nouă nu poate fi aceeași cu parola actuală + UsernameOrPassword: + Invalid: Numele de utilizator sau parola sunt nevalide + PasswordComplexityPolicy: + NotFound: Politica de parolă nu a fost găsită + MinLength: Parola este prea scurtă + HasLower: Parola trebuie să conțină o literă mică + HasUpper: Parola trebuie să conțină o literă mare + HasNumber: Parola trebuie să conțină un număr + HasSymbol: Parola trebuie să conțină un simbol + Code: + Expired: Codul a expirat + Invalid: Codul este nevalid + Empty: Codul este gol + CryptoCodeNil: Codul cripto este nul + NotFound: Nu s-a putut găsi codul + GeneratorAlgNotSupported: Algoritmul generator nesuportat + EmailVerify: + UserIDEmpty: UserID este gol + ExternalData: + CouldNotRead: Datele externe nu au putut fi citite corect + MFA: + NoProviders: Nu există furnizori multifactoriali disponibili + OTP: + AlreadyReady: MFA OTP (OneTimePassword) este deja configurat + NotExisting: Multifactor OTP (OneTimePassword) nu există + InvalidCode: Cod nevalid + NotReady: Multifactor OTP (OneTimePassword) nu este gata + Locked: Utilizatorul este blocat + SomethingWentWrong: Ceva nu a mers bine + NotActive: Utilizatorul nu este activ + ExternalIDP: + IDPTypeNotImplemented: Tipul IDP nu este implementat + NotAllowed: Furnizorul extern de autentificare nu este permis + IDPConfigIDEmpty: ID-ul de configurare al furnizorului de identitate este gol + ExternalUserIDEmpty: ID-ul utilizatorului extern este gol + UserDisplayNameEmpty: Numele de afișare al utilizatorului este gol + NoExternalUserData: Nu s-au primit date externe de utilizator + CreationNotAllowed: Crearea unui nou utilizator nu este permisă la acest furnizor + LinkingNotAllowed: Asocierea unui utilizator nu este permisă la acest furnizor + NoOptionAllowed: Nici crearea, nici asocierea nu sunt permise la acest furnizor. Vă rugăm să contactați administratorul. + LoginFailedSwitchLocal: | + Autentificarea la IDP extern a eșuat. Se revine la autentificarea locală. + + Detalii despre eroare: {{.Details}} + GrantRequired: Autentificarea nu este posibilă. Utilizatorul trebuie să aibă cel puțin un drept de acces la aplicație. Vă rugăm să contactați administratorul. + ProjectRequired: Autentificarea nu este posibilă. Organizația utilizatorului trebuie să fie autorizată pentru proiect. Vă rugăm să contactați administratorul. + IdentityProvider: + InvalidConfig: Configurația furnizorului de identitate este nevalidă + IAM: + LockoutPolicy: + NotExisting: Politica de blocare nu există + Org: + LoginPolicy: + RegistrationNotAllowed: Înregistrarea nu este permisă + DeviceAuth: + NotExisting: Codul de utilizator nu există + +optional: (opțional) diff --git a/internal/api/ui/login/static/i18n/ru.yaml b/internal/api/ui/login/static/i18n/ru.yaml index 8afd3a31b6..7d5c2b0f98 100644 --- a/internal/api/ui/login/static/i18n/ru.yaml +++ b/internal/api/ui/login/static/i18n/ru.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Пол Female: Женский Male: Мужской @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Условия использования TosConfirm: Я согласен с TosLinkText: Пользовательским соглашением @@ -385,7 +387,8 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 - + Romanian: Română + DeviceAuth: Title: Авторизация устройства UserCode: diff --git a/internal/api/ui/login/static/i18n/sv.yaml b/internal/api/ui/login/static/i18n/sv.yaml index e6c1245503..f7398465c0 100644 --- a/internal/api/ui/login/static/i18n/sv.yaml +++ b/internal/api/ui/login/static/i18n/sv.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: Kön Female: Man Male: Kvinna @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: Användarvillkor TosConfirm: Jag accepterar TosLinkText: Användarvillkoren @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: Tillgång från hårdvaruenhet UserCode: diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index 4fcb469831..4ba5904700 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -265,6 +265,7 @@ RegistrationUser: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română GenderLabel: 性别 Female: 女性 Male: 男性 @@ -308,6 +309,7 @@ ExternalRegistrationUserOverview: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română TosAndPrivacyLabel: 条款和条款 TosConfirm: 我接受 TosLinkText: 服务条款 @@ -385,6 +387,7 @@ ExternalNotFound: Indonesian: Bahasa Indonesia Hungarian: Magyar Korean: 한국어 + Romanian: Română DeviceAuth: Title: 设备授权 UserCode: diff --git a/internal/api/ui/login/static/resources/scripts/webauthn.js b/internal/api/ui/login/static/resources/scripts/webauthn.js index e8d9c54f41..f44f3db498 100644 --- a/internal/api/ui/login/static/resources/scripts/webauthn.js +++ b/internal/api/ui/login/static/resources/scripts/webauthn.js @@ -1,4 +1,4 @@ -function checkWebauthnSupported(button, func) { +function checkWebauthnSupported(func, optionalClickId) { let support = document.getElementsByClassName("wa-support"); let noSupport = document.getElementsByClassName("wa-no-support"); if (!window.PublicKeyCredential) { @@ -10,12 +10,28 @@ function checkWebauthnSupported(button, func) { } return; } - document.getElementById(button).addEventListener("click", func); + + // if id is provided add click event only, otherwise call the function directly + if (optionalClickId) { + document.getElementById(optionalClickId).addEventListener("click", func); + } else { + func(); + } } function webauthnError(error) { let err = document.getElementById("wa-error"); - err.getElementsByClassName("cause")[0].innerText = error.message; + let causeElement = err.getElementsByClassName("cause")[0]; + + if (error.message) { + causeElement.innerText = error.message; + } else if (error.value) { + causeElement.innerText = error.value; + } else { + console.error("Unknown error:", error); + causeElement.innerText = "An unknown error occurred."; + } + err.classList.remove("hidden"); } diff --git a/internal/api/ui/login/static/resources/scripts/webauthn_login.js b/internal/api/ui/login/static/resources/scripts/webauthn_login.js index 4265d76e62..d8af4cd02f 100644 --- a/internal/api/ui/login/static/resources/scripts/webauthn_login.js +++ b/internal/api/ui/login/static/resources/scripts/webauthn_login.js @@ -1,31 +1,38 @@ document.addEventListener( "DOMContentLoaded", - checkWebauthnSupported("btn-login", login) + checkWebauthnSupported(login, "btn-login"), ); -function login() { +async function login() { document.getElementById("wa-error").classList.add("hidden"); - let makeAssertionOptions = JSON.parse( - atob(document.getElementsByName("credentialAssertionData")[0].value) - ); - makeAssertionOptions.publicKey.challenge = bufferDecode( - makeAssertionOptions.publicKey.challenge, - "publicKey.challenge" - ); - makeAssertionOptions.publicKey.allowCredentials.forEach(function (listItem) { - listItem.id = bufferDecode(listItem.id, "publicKey.allowCredentials.id"); - }); - navigator.credentials - .get({ - publicKey: makeAssertionOptions.publicKey, - }) - .then(function (credential) { - verifyAssertion(credential); - }) - .catch(function (err) { - webauthnError(err); + let makeAssertionOptions; + try { + makeAssertionOptions = JSON.parse(atob(document.getElementsByName("credentialAssertionData")[0].value)); + } catch (e) { + webauthnError({ message: "Failed to parse credential assertion data." }); + return; + } + + try { + makeAssertionOptions.publicKey.challenge = bufferDecode(makeAssertionOptions.publicKey.challenge, "publicKey.challenge"); + makeAssertionOptions.publicKey.allowCredentials.forEach(function (listItem) { + listItem.id = bufferDecode(listItem.id, "publicKey.allowCredentials.id"); }); + } catch (e) { + webauthnError({ message: "Failed to decode buffer data." }); + return; + } + + try { + const credential = await navigator.credentials.get({ + publicKey: makeAssertionOptions.publicKey, + }); + + verifyAssertion(credential); + } catch (err) { + webauthnError(err); + } } function verifyAssertion(assertedCredential) { @@ -49,6 +56,6 @@ function verifyAssertion(assertedCredential) { }, }); - document.getElementsByName("credentialData")[0].value = btoa(data); + document.getElementsByName("credentialData")[0].value = window.btoa(data); document.getElementsByTagName("form")[0].submit(); } diff --git a/internal/api/ui/login/static/resources/scripts/webauthn_register.js b/internal/api/ui/login/static/resources/scripts/webauthn_register.js index 7ce875905b..49385d96fc 100644 --- a/internal/api/ui/login/static/resources/scripts/webauthn_register.js +++ b/internal/api/ui/login/static/resources/scripts/webauthn_register.js @@ -1,42 +1,51 @@ document.addEventListener( "DOMContentLoaded", - checkWebauthnSupported("btn-register", registerCredential) -); - -function registerCredential() { - document.getElementById("wa-error").classList.add("hidden"); - - let opt = JSON.parse( - atob(document.getElementsByName("credentialCreationData")[0].value) - ); - opt.publicKey.challenge = bufferDecode( - opt.publicKey.challenge, - "publicKey.challenge" - ); - opt.publicKey.user.id = bufferDecode( - opt.publicKey.user.id, - "publicKey.user.id" - ); - if (opt.publicKey.excludeCredentials) { - for (let i = 0; i < opt.publicKey.excludeCredentials.length; i++) { - if (opt.publicKey.excludeCredentials[i].id !== null) { - opt.publicKey.excludeCredentials[i].id = bufferDecode( - opt.publicKey.excludeCredentials[i].id, - "publicKey.excludeCredentials" - ); - } + () => { + const form = document.getElementsByTagName("form")[0]; + if (form) { + form.addEventListener("submit", (event) => { + event.preventDefault(); // Prevent the default form submission + checkWebauthnSupported(registerCredential); + }); } } - navigator.credentials - .create({ +); + +async function registerCredential() { + document.getElementById("wa-error").classList.add("hidden"); + + let opt; + try { + opt = JSON.parse(window.atob(document.getElementsByName("credentialCreationData")[0].value)); + } catch (e) { + webauthnError({ message: "Failed to parse credential creation data." }); + return; + } + + try { + opt.publicKey.challenge = bufferDecode(opt.publicKey.challenge, "publicKey.challenge"); + opt.publicKey.user.id = bufferDecode(opt.publicKey.user.id, "publicKey.user.id"); + if (opt.publicKey.excludeCredentials) { + for (let i = 0; i < opt.publicKey.excludeCredentials.length; i++) { + if (opt.publicKey.excludeCredentials[i].id !== null) { + opt.publicKey.excludeCredentials[i].id = bufferDecode(opt.publicKey.excludeCredentials[i].id, "publicKey.excludeCredentials"); + } + } + } + } catch (e) { + webauthnError({ message: "Failed to decode buffer data." }); + return; + } + + try { + const credential = await navigator.credentials.create({ publicKey: opt.publicKey, - }) - .then(function (credential) { - createCredential(credential); - }) - .catch(function (err) { - webauthnError(err); }); + + createCredential(credential); + } catch (err) { + webauthnError(err); + } } function createCredential(newCredential) { @@ -56,6 +65,6 @@ function createCredential(newCredential) { }, }); - document.getElementsByName("credentialData")[0].value = btoa(data); + document.getElementsByName("credentialData")[0].value = window.btoa(data); document.getElementsByTagName("form")[0].submit(); } diff --git a/internal/api/ui/login/static/resources/themes/scss/styles/button/button_base.scss b/internal/api/ui/login/static/resources/themes/scss/styles/button/button_base.scss index dd53dceb79..aeeeba541c 100644 --- a/internal/api/ui/login/static/resources/themes/scss/styles/button/button_base.scss +++ b/internal/api/ui/login/static/resources/themes/scss/styles/button/button_base.scss @@ -39,7 +39,9 @@ $lgn-icon-button-line-height: 40px !default; padding: $lgn-button-padding; border-radius: $lgn-button-border-radius; - overflow: visible; + overflow: hidden; + text-overflow: ellipsis; + transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); &[disabled] { diff --git a/internal/api/ui/login/static/templates/external_not_found_option.html b/internal/api/ui/login/static/templates/external_not_found_option.html index 33bcaeb4e0..dca4bd5edc 100644 --- a/internal/api/ui/login/static/templates/external_not_found_option.html +++ b/internal/api/ui/login/static/templates/external_not_found_option.html @@ -100,6 +100,8 @@ +
diff --git a/internal/api/ui/login/static/templates/mfa_init_u2f.html b/internal/api/ui/login/static/templates/mfa_init_u2f.html index 826defaa39..a0c3902342 100644 --- a/internal/api/ui/login/static/templates/mfa_init_u2f.html +++ b/internal/api/ui/login/static/templates/mfa_init_u2f.html @@ -37,7 +37,7 @@ - +
diff --git a/internal/api/ui/login/static/templates/passwordless_registration.html b/internal/api/ui/login/static/templates/passwordless_registration.html index 5ba814d66f..cfe65d182f 100644 --- a/internal/api/ui/login/static/templates/passwordless_registration.html +++ b/internal/api/ui/login/static/templates/passwordless_registration.html @@ -40,7 +40,7 @@
{{if not .Disabled}} - + {{end}}
diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go index c16a757a01..53272d2d2f 100644 --- a/internal/auth/repository/auth_request.go +++ b/internal/auth/repository/auth_request.go @@ -20,7 +20,7 @@ type AuthRequestRepository interface { SetExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *domain.ExternalUser) error SetLinkingUser(ctx context.Context, request *domain.AuthRequest, externalUser *domain.ExternalUser) error SelectUser(ctx context.Context, authReqID, userID, userAgentID string) error - SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error + SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string, idpArguments map[string]any) error VerifyPassword(ctx context.Context, id, userID, resourceOwner, password, userAgentID string, info *domain.BrowserInfo) error VerifyMFAOTP(ctx context.Context, authRequestID, userID, resourceOwner, code, userAgentID string, info *domain.BrowserInfo) error diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 60486b66f9..0ede13ae68 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -255,14 +255,14 @@ func (repo *AuthRequestRepo) CheckLoginName(ctx context.Context, id, loginName, return repo.AuthRequests.UpdateAuthRequest(ctx, request) } -func (repo *AuthRequestRepo) SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) (err error) { +func (repo *AuthRequestRepo) SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string, idpArguments map[string]any) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) if err != nil { return err } - err = repo.checkSelectedExternalIDP(request, idpConfigID) + err = repo.checkSelectedExternalIDP(request, idpConfigID, idpArguments) if err != nil { return err } @@ -789,7 +789,7 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain } // if there's an active (human) user, let's use it if user != nil && !user.HumanView.IsZero() && domain.UserState(user.State).IsEnabled() { - request.SetUserInfo(user.ID, loginNameInput, user.PreferredLoginName, "", "", user.ResourceOwner) + request.SetUserInfo(user.ID, loginNameInput, preferredLoginName, "", "", user.ResourceOwner) return nil } // the user was either not found or not active @@ -984,10 +984,11 @@ func queryLoginPolicyToDomain(policy *query.LoginPolicy) *domain.LoginPolicy { } } -func (repo *AuthRequestRepo) checkSelectedExternalIDP(request *domain.AuthRequest, idpConfigID string) error { +func (repo *AuthRequestRepo) checkSelectedExternalIDP(request *domain.AuthRequest, idpConfigID string, idpArguments map[string]any) error { for _, externalIDP := range request.AllowedExternalIDPs { if externalIDP.IDPConfigID == idpConfigID { request.SelectedIDPConfigID = idpConfigID + request.SelectedIDPConfigArgs = idpArguments return nil } } @@ -1054,9 +1055,6 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth if err != nil { return nil, err } - if user.PreferredLoginName != "" { - request.LoginName = user.PreferredLoginName - } userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user) if err != nil { return nil, err diff --git a/internal/auth/repository/eventsourcing/eventstore/org.go b/internal/auth/repository/eventsourcing/eventstore/org.go index 938f0d27cd..78c69d63c9 100644 --- a/internal/auth/repository/eventsourcing/eventstore/org.go +++ b/internal/auth/repository/eventsourcing/eventstore/org.go @@ -23,7 +23,7 @@ type OrgRepository struct { } func (repo *OrgRepository) GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error) { - policy, err := repo.Query.PasswordComplexityPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID, false) + policy, err := repo.Query.PasswordComplexityPolicyByOrg(ctx, false, authz.GetCtxData(ctx).OrgID, false) if err != nil { return nil, err } diff --git a/internal/auth/repository/eventsourcing/view/view.go b/internal/auth/repository/eventsourcing/view/view.go index 56e2676b87..c67844dbad 100644 --- a/internal/auth/repository/eventsourcing/view/view.go +++ b/internal/auth/repository/eventsourcing/view/view.go @@ -1,11 +1,8 @@ package view import ( - "context" - "github.com/jinzhu/gorm" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" @@ -37,7 +34,3 @@ func StartView(sqlClient *database.DB, keyAlgorithm crypto.EncryptionAlgorithm, func (v *View) Health() (err error) { return v.Db.DB().Ping() } - -func (v *View) TimeTravel(ctx context.Context, tableName string) string { - return tableName + v.client.Timetravel(call.Took(ctx)) -} diff --git a/internal/authz/repository/eventsourcing/view/view.go b/internal/authz/repository/eventsourcing/view/view.go index f25b764f53..21a15c45fc 100644 --- a/internal/authz/repository/eventsourcing/view/view.go +++ b/internal/authz/repository/eventsourcing/view/view.go @@ -1,11 +1,8 @@ package view import ( - "context" - "github.com/jinzhu/gorm" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/query" ) @@ -31,7 +28,3 @@ func StartView(sqlClient *database.DB, queries *query.Queries) (*View, error) { func (v *View) Health() (err error) { return v.Db.DB().Ping() } - -func (v *View) TimeTravel(ctx context.Context, tableName string) string { - return tableName + v.client.Timetravel(call.Took(ctx)) -} diff --git a/internal/cache/connector/pg/connector.go b/internal/cache/connector/pg/connector.go index 9a89cf5f6a..e919aea49d 100644 --- a/internal/cache/connector/pg/connector.go +++ b/internal/cache/connector/pg/connector.go @@ -12,8 +12,7 @@ type Config struct { type Connector struct { PGXPool - Dialect string - Config Config + Config Config } func NewConnector(config Config, client *database.DB) *Connector { @@ -22,7 +21,6 @@ func NewConnector(config Config, client *database.DB) *Connector { } return &Connector{ PGXPool: client.Pool, - Dialect: client.Type(), Config: config, } } diff --git a/internal/cache/connector/pg/pg.go b/internal/cache/connector/pg/pg.go index 18215b68ed..530205f871 100644 --- a/internal/cache/connector/pg/pg.go +++ b/internal/cache/connector/pg/pg.go @@ -5,12 +5,12 @@ import ( _ "embed" "errors" "log/slog" + "slices" "strings" "text/template" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "golang.org/x/exp/slices" "github.com/zitadel/zitadel/internal/cache" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -58,10 +58,8 @@ func NewCache[I ~int, K ~string, V cache.Entry[I, K]](ctx context.Context, purpo } c.logger.InfoContext(ctx, "pg cache logging enabled") - if connector.Dialect == "postgres" { - if err := c.createPartition(ctx); err != nil { - return nil, err - } + if err := c.createPartition(ctx); err != nil { + return nil, err } return c, nil } diff --git a/internal/cache/connector/pg/pg_test.go b/internal/cache/connector/pg/pg_test.go index f5980ad845..bb9b681b15 100644 --- a/internal/cache/connector/pg/pg_test.go +++ b/internal/cache/connector/pg/pg_test.go @@ -78,7 +78,6 @@ func TestNewCache(t *testing.T) { tt.expect(pool) connector := &Connector{ PGXPool: pool, - Dialect: "postgres", } c, err := NewCache[testIndex, string, *testObject](context.Background(), cachePurpose, conf, testIndices, connector) @@ -518,7 +517,6 @@ func prepareCache(t *testing.T, conf cache.Config) (cache.PrunerCache[testIndex, WillReturnResult(pgxmock.NewResult("CREATE TABLE", 0)) connector := &Connector{ PGXPool: pool, - Dialect: "postgres", } c, err := NewCache[testIndex, string, *testObject](context.Background(), cachePurpose, conf, testIndices, connector) require.NoError(t, err) diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go index 6833125a0a..d41ea3f2d5 100644 --- a/internal/command/action_v2_execution_test.go +++ b/internal/command/action_v2_execution_test.go @@ -20,6 +20,7 @@ func existsMock(exists bool) func(method string) bool { return exists } } + func TestCommands_SetExecutionRequest(t *testing.T) { type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore diff --git a/internal/command/action_v2_target.go b/internal/command/action_v2_target.go index 95dd097ed0..3fb374b36f 100644 --- a/internal/command/action_v2_target.go +++ b/internal/command/action_v2_target.go @@ -40,31 +40,31 @@ func (a *AddTarget) IsValid() error { return nil } -func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner string) (_ *domain.ObjectDetails, err error) { +func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner string) (_ time.Time, err error) { if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-brml926e2d", "Errors.IDMissing") + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-brml926e2d", "Errors.IDMissing") } if err := add.IsValid(); err != nil { - return nil, err + return time.Time{}, err } if add.AggregateID == "" { add.AggregateID, err = c.idGenerator.Next() if err != nil { - return nil, err + return time.Time{}, err } } wm, err := c.getTargetWriteModelByID(ctx, add.AggregateID, resourceOwner) if err != nil { - return nil, err + return time.Time{}, err } if wm.State.Exists() { - return nil, zerrors.ThrowAlreadyExists(nil, "INSTANCE-9axkz0jvzm", "Errors.Target.AlreadyExists") + return time.Time{}, zerrors.ThrowAlreadyExists(nil, "INSTANCE-9axkz0jvzm", "Errors.Target.AlreadyExists") } code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.targetEncryption) //nolint if err != nil { - return nil, err + return time.Time{}, err } add.SigningKey = code.PlainCode() pushedEvents, err := c.eventstore.Push(ctx, target.NewAddedEvent( @@ -78,12 +78,12 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner code.Crypted, )) if err != nil { - return nil, err + return time.Time{}, err } if err := AppendAndReduce(wm, pushedEvents...); err != nil { - return nil, err + return time.Time{}, err } - return writeModelToObjectDetails(&wm.WriteModel), nil + return wm.ChangeDate, nil } type ChangeTarget struct { @@ -118,26 +118,26 @@ func (a *ChangeTarget) IsValid() error { return nil } -func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resourceOwner string) (time.Time, error) { if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-zqibgg0wwh", "Errors.IDMissing") + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-zqibgg0wwh", "Errors.IDMissing") } if err := change.IsValid(); err != nil { - return nil, err + return time.Time{}, err } existing, err := c.getTargetWriteModelByID(ctx, change.AggregateID, resourceOwner) if err != nil { - return nil, err + return time.Time{}, err } if !existing.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound") + return time.Time{}, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound") } var changedSigningKey *crypto.CryptoValue if change.ExpirationSigningKey { code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.targetEncryption) //nolint if err != nil { - return nil, err + return time.Time{}, err } changedSigningKey = code.Crypted change.SigningKey = &code.Plain @@ -154,30 +154,30 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou changedSigningKey, ) if changedEvent == nil { - return writeModelToObjectDetails(&existing.WriteModel), nil + return existing.WriteModel.ChangeDate, nil } pushedEvents, err := c.eventstore.Push(ctx, changedEvent) if err != nil { - return nil, err + return time.Time{}, err } err = AppendAndReduce(existing, pushedEvents...) if err != nil { - return nil, err + return time.Time{}, err } - return writeModelToObjectDetails(&existing.WriteModel), nil + return existing.WriteModel.ChangeDate, nil } -func (c *Commands) DeleteTarget(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) DeleteTarget(ctx context.Context, id, resourceOwner string) (time.Time, error) { if id == "" || resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing") + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing") } existing, err := c.getTargetWriteModelByID(ctx, id, resourceOwner) if err != nil { - return nil, err + return time.Time{}, err } if !existing.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-k4s7ucu0ax", "Errors.Target.NotFound") + return existing.WriteModel.ChangeDate, nil } if err := c.pushAppendAndReduce(ctx, @@ -187,9 +187,9 @@ func (c *Commands) DeleteTarget(ctx context.Context, id, resourceOwner string) ( existing.Name, ), ); err != nil { - return nil, err + return time.Time{}, err } - return writeModelToObjectDetails(&existing.WriteModel), nil + return existing.WriteModel.ChangeDate, nil } func (c *Commands) existsTargetsByIDs(ctx context.Context, ids []string, resourceOwner string) bool { diff --git a/internal/command/action_v2_target_test.go b/internal/command/action_v2_target_test.go index ed7d6163a0..32ecbff93a 100644 --- a/internal/command/action_v2_target_test.go +++ b/internal/command/action_v2_target_test.go @@ -31,9 +31,8 @@ func TestCommands_AddTarget(t *testing.T) { resourceOwner string } type res struct { - id string - details *domain.ObjectDetails - err func(error) bool + id string + err func(error) bool } tests := []struct { name string @@ -213,10 +212,6 @@ func TestCommands_AddTarget(t *testing.T) { }, res{ id: "id1", - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, }, }, { @@ -249,10 +244,6 @@ func TestCommands_AddTarget(t *testing.T) { }, res{ id: "id1", - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, }, }, } @@ -264,7 +255,7 @@ func TestCommands_AddTarget(t *testing.T) { newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, defaultSecretGenerators: tt.fields.defaultSecretGenerators, } - details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner) + _, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -273,7 +264,6 @@ func TestCommands_AddTarget(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, tt.args.add.AggregateID) - assertObjectDetails(t, tt.res.details, details) } }) } @@ -291,8 +281,7 @@ func TestCommands_ChangeTarget(t *testing.T) { resourceOwner string } type res struct { - details *domain.ObjectDetails - err func(error) bool + err func(error) bool } tests := []struct { name string @@ -434,12 +423,7 @@ func TestCommands_ChangeTarget(t *testing.T) { }, resourceOwner: "instance", }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, - }, + res{}, }, { "unique constraint failed, error", @@ -504,12 +488,7 @@ func TestCommands_ChangeTarget(t *testing.T) { }, resourceOwner: "instance", }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, - }, + res{}, }, { "push full ok", @@ -557,12 +536,7 @@ func TestCommands_ChangeTarget(t *testing.T) { }, resourceOwner: "instance", }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, - }, + res{}, }, } for _, tt := range tests { @@ -572,16 +546,13 @@ func TestCommands_ChangeTarget(t *testing.T) { newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, defaultSecretGenerators: tt.fields.defaultSecretGenerators, } - details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner) + _, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } if tt.res.err != nil && !tt.res.err(err) { t.Errorf("got wrong err: %v ", err) } - if tt.res.err == nil { - assertObjectDetails(t, tt.res.details, details) - } }) } } @@ -596,8 +567,7 @@ func TestCommands_DeleteTarget(t *testing.T) { resourceOwner string } type res struct { - details *domain.ObjectDetails - err func(error) bool + err func(error) bool } tests := []struct { name string @@ -631,9 +601,7 @@ func TestCommands_DeleteTarget(t *testing.T) { id: "id1", resourceOwner: "instance", }, - res{ - err: zerrors.IsNotFound, - }, + res{}, }, { "remove ok", @@ -657,12 +625,31 @@ func TestCommands_DeleteTarget(t *testing.T) { id: "id1", resourceOwner: "instance", }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - ID: "id1", - }, + res{}, + }, + { + "already removed", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + targetAddEvent("id1", "instance"), + ), + eventFromEventPusher( + target.NewRemovedEvent(context.Background(), + target.NewAggregate("id1", "instance"), + "name", + ), + ), + ), + ), }, + args{ + ctx: context.Background(), + id: "id1", + resourceOwner: "instance", + }, + res{}, }, } for _, tt := range tests { @@ -670,16 +657,13 @@ func TestCommands_DeleteTarget(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), } - details, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner) + _, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } if tt.res.err != nil && !tt.res.err(err) { t.Errorf("got wrong err: %v ", err) } - if tt.res.err == nil { - assertObjectDetails(t, tt.res.details, details) - } }) } } diff --git a/internal/command/auth_request.go b/internal/command/auth_request.go index 340155d11b..d60012637a 100644 --- a/internal/command/auth_request.go +++ b/internal/command/auth_request.go @@ -29,6 +29,7 @@ type AuthRequest struct { LoginHint *string HintUserID *string NeedRefreshToken bool + Issuer string } type CurrentAuthRequest struct { @@ -73,6 +74,7 @@ func (c *Commands) AddAuthRequest(ctx context.Context, authRequest *AuthRequest) authRequest.LoginHint, authRequest.HintUserID, authRequest.NeedRefreshToken, + authRequest.Issuer, )) if err != nil { return nil, err @@ -180,6 +182,7 @@ func authRequestWriteModelToCurrentAuthRequest(writeModel *AuthRequestWriteModel MaxAge: writeModel.MaxAge, LoginHint: writeModel.LoginHint, HintUserID: writeModel.HintUserID, + Issuer: writeModel.Issuer, }, SessionID: writeModel.SessionID, UserID: writeModel.UserID, diff --git a/internal/command/auth_request_model.go b/internal/command/auth_request_model.go index a6766d1979..0e8d88fd13 100644 --- a/internal/command/auth_request_model.go +++ b/internal/command/auth_request_model.go @@ -36,6 +36,7 @@ type AuthRequestWriteModel struct { AuthMethods []domain.UserAuthMethodType AuthRequestState domain.AuthRequestState NeedRefreshToken bool + Issuer string } func NewAuthRequestWriteModel(ctx context.Context, id string) *AuthRequestWriteModel { @@ -68,6 +69,7 @@ func (m *AuthRequestWriteModel) Reduce() error { m.HintUserID = e.HintUserID m.AuthRequestState = domain.AuthRequestStateAdded m.NeedRefreshToken = e.NeedRefreshToken + m.Issuer = e.Issuer case *authrequest.SessionLinkedEvent: m.SessionID = e.SessionID m.UserID = e.UserID diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index 590e4086f4..c0b5f630f7 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -62,6 +62,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { nil, nil, false, + "issuer", ), ), ), @@ -101,6 +102,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), false, + "issuer", ), ), ), @@ -127,6 +129,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { MaxAge: gu.Ptr(time.Duration(0)), LoginHint: gu.Ptr("loginHint"), HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", }, }, &CurrentAuthRequest{ @@ -150,6 +153,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { MaxAge: gu.Ptr(time.Duration(0)), LoginHint: gu.Ptr("loginHint"), HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", }, }, nil, @@ -234,6 +238,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), eventFromEventPusher( @@ -276,6 +281,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -317,6 +323,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -356,6 +363,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -418,6 +426,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -469,6 +478,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -527,6 +537,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -557,6 +568,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -616,6 +628,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -646,6 +659,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -706,6 +720,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -736,6 +751,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -797,6 +813,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -827,6 +844,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -950,6 +968,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -978,6 +997,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, }, }, @@ -1050,6 +1070,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), ), @@ -1088,6 +1109,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( diff --git a/internal/command/command.go b/internal/command/command.go index ab047fccdb..b0e67ad52e 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -9,7 +9,9 @@ import ( "fmt" "math/big" "net/http" + "slices" "strconv" + "strings" "sync" "time" @@ -177,13 +179,18 @@ func StartCommands( defaultSecretGenerators: defaultSecretGenerators, samlCertificateAndKeyGenerator: samlCertificateAndKeyGenerator(defaults.KeyConfig.CertificateSize, defaults.KeyConfig.CertificateLifetime), webKeyGenerator: crypto.GenerateEncryptedWebKey, - // always true for now until we can check with an eventlist - EventExisting: func(event string) bool { return true }, - // always true for now until we can check with an eventlist - EventGroupExisting: func(group string) bool { return true }, + EventExisting: func(value string) bool { + return slices.Contains(es.EventTypes(), value) + }, + EventGroupExisting: func(group string) bool { + return slices.ContainsFunc(es.EventTypes(), func(value string) bool { + return strings.HasPrefix(value, group) + }, + ) + }, GrpcServiceExisting: func(service string) bool { return false }, GrpcMethodExisting: func(method string) bool { return false }, - ActionFunctionExisting: domain.FunctionExists(), + ActionFunctionExisting: domain.ActionFunctionExists(), multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ CryptoMFA: otpEncryption, diff --git a/internal/command/converter.go b/internal/command/converter.go index e4309a54c1..292de21aa9 100644 --- a/internal/command/converter.go +++ b/internal/command/converter.go @@ -15,6 +15,9 @@ func writeModelToObjectDetails(writeModel *eventstore.WriteModel) *domain.Object } func pushedEventsToObjectDetails(events []eventstore.Event) *domain.ObjectDetails { + if len(events) == 0 { + return &domain.ObjectDetails{} + } return &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreatedAt(), diff --git a/internal/command/device_auth.go b/internal/command/device_auth.go index a2754650ea..ef6b069cc9 100644 --- a/internal/command/device_auth.go +++ b/internal/command/device_auth.go @@ -59,6 +59,9 @@ func (c *Commands) ApproveDeviceAuth( if !model.State.Exists() { return nil, zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound") } + if model.State != domain.DeviceAuthStateInitiated { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-GEJL3", "Errors.DeviceAuth.AlreadyHandled") + } pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent, sessionID)) if err != nil { return nil, err @@ -71,6 +74,60 @@ func (c *Commands) ApproveDeviceAuth( return writeModelToObjectDetails(&model.WriteModel), nil } +func (c *Commands) ApproveDeviceAuthWithSession( + ctx context.Context, + deviceCode, + sessionID, + sessionToken string, +) (*domain.ObjectDetails, error) { + model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode) + if err != nil { + return nil, err + } + if !model.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-D2hf2", "Errors.DeviceAuth.NotFound") + } + if model.State != domain.DeviceAuthStateInitiated { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-D30Jf", "Errors.DeviceAuth.AlreadyHandled") + } + if err := c.checkPermission(ctx, domain.PermissionSessionLink, model.ResourceOwner, ""); err != nil { + return nil, err + } + + sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()) + err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) + if err != nil { + return nil, err + } + if err = sessionWriteModel.CheckIsActive(); err != nil { + return nil, err + } + if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil { + return nil, err + } + + pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent( + ctx, + model.aggregate, + sessionWriteModel.UserID, + sessionWriteModel.UserResourceOwner, + sessionWriteModel.AuthMethodTypes(), + sessionWriteModel.AuthenticationTime(), + sessionWriteModel.PreferredLanguage, + sessionWriteModel.UserAgent, + sessionID, + )) + if err != nil { + return nil, err + } + err = AppendAndReduce(model, pushedEvents...) + if err != nil { + return nil, err + } + + return writeModelToObjectDetails(&model.WriteModel), nil +} + func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domain.DeviceAuthCanceled) (*domain.ObjectDetails, error) { model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, id) if err != nil { @@ -117,7 +174,7 @@ func (e DeviceAuthStateError) Error() string { // As devices can poll at various intervals, an explicit state takes precedence over expiry. // This is to prevent cases where users might approve or deny the authorization on time, but the next poll // happens after expiry. -func (c *Commands) CreateOIDCSessionFromDeviceAuth(ctx context.Context, deviceCode string) (_ *OIDCSession, err error) { +func (c *Commands) CreateOIDCSessionFromDeviceAuth(ctx context.Context, deviceCode, backChannelLogoutURI string) (_ *OIDCSession, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -162,6 +219,7 @@ func (c *Commands) CreateOIDCSessionFromDeviceAuth(ctx context.Context, deviceCo deviceAuthModel.PreferredLanguage, deviceAuthModel.UserAgent, ) + cmd.RegisterLogout(ctx, deviceAuthModel.SessionID, deviceAuthModel.UserID, deviceAuthModel.ClientID, backChannelLogoutURI) if err = cmd.AddAccessToken(ctx, deviceAuthModel.Scopes, deviceAuthModel.UserID, deviceAuthModel.UserOrgID, domain.TokenReasonAuthRequest, nil); err != nil { return nil, err } diff --git a/internal/command/device_auth_model.go b/internal/command/device_auth_model.go index 21ab1b29ec..28833ae898 100644 --- a/internal/command/device_auth_model.go +++ b/internal/command/device_auth_model.go @@ -82,6 +82,7 @@ func (m *DeviceAuthWriteModel) Query() *eventstore.SearchQueryBuilder { deviceauth.AddedEventType, deviceauth.ApprovedEventType, deviceauth.CanceledEventType, + deviceauth.DoneEventType, ). Builder() } diff --git a/internal/command/device_auth_test.go b/internal/command/device_auth_test.go index f25be7053a..021ae25d36 100644 --- a/internal/command/device_auth_test.go +++ b/internal/command/device_auth_test.go @@ -19,10 +19,13 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id/mock" "github.com/zitadel/zitadel/internal/repository/deviceauth" "github.com/zitadel/zitadel/internal/repository/oidcsession" + "github.com/zitadel/zitadel/internal/repository/session" + "github.com/zitadel/zitadel/internal/repository/sessionlogout" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -265,6 +268,310 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { } } +func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { + ctx := authz.WithInstanceID(context.Background(), "instance1") + now := time.Now() + pushErr := errors.New("pushErr") + + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + deviceCode string + sessionID string + sessionToken string + } + tests := []struct { + name string + fields fields + args args + wantDetails *domain.ObjectDetails + wantErr error + }{ + { + name: "not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx, + "notfound", + "sessionID", + "sessionToken", + }, + wantErr: zerrors.ThrowNotFound(nil, "COMMAND-D2hf2", "Errors.DeviceAuth.NotFound"), + }, + { + name: "not initialized, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, + ), + ), + eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewCanceledEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + domain.DeviceAuthCanceledDenied, + )), + ), + ), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D30Jf", "Errors.DeviceAuth.AlreadyHandled"), + }, + { + name: "missing permission, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, + ), + )), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "session not active, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, + ), + )), + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"), + }, + { + name: "invalid session token, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, + ), + )), + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + session.NewAddedEvent(ctx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + )), + tokenVerifier: newMockTokenVerifierInvalid(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "invalidToken", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"), + }, + { + name: "push error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, + ), + )), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(ctx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + eventFromEventPusher( + session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "orgID", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + expectPushFailed(pushErr, + deviceauth.NewApprovedEvent( + ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + testNow, &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantErr: pushErr, + }, + { + name: "authorized", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, + ), + )), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(ctx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + eventFromEventPusher( + session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "orgID", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + expectPush( + deviceauth.NewApprovedEvent( + ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + testNow, &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + sessionTokenVerifier: tt.fields.tokenVerifier, + checkPermission: tt.fields.checkPermission, + } + gotDetails, err := c.ApproveDeviceAuthWithSession(tt.args.ctx, tt.args.deviceCode, tt.args.sessionID, tt.args.sessionToken) + require.ErrorIs(t, err, tt.wantErr) + assertObjectDetails(t, tt.wantDetails, gotDetails) + }) + } +} + func TestCommands_CancelDeviceAuth(t *testing.T) { ctx := authz.WithInstanceID(context.Background(), "instance1") now := time.Now() @@ -399,8 +706,9 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { keyAlgorithm crypto.EncryptionAlgorithm } type args struct { - ctx context.Context - deviceCode string + ctx context.Context + deviceCode string + backChannelLogoutURI string } tests := []struct { name string @@ -419,6 +727,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "device1", + "", }, wantErr: io.ErrClosedPipe, }, @@ -443,6 +752,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", }, wantErr: DeviceAuthStateError(domain.DeviceAuthStateInitiated), }, @@ -456,6 +766,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", }, wantErr: zerrors.ThrowNotFound(nil, "COMMAND-ua1Vo", "Errors.DeviceAuth.NotFound"), }, @@ -484,6 +795,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", }, wantErr: DeviceAuthStateError(domain.DeviceAuthStateExpired), }, @@ -515,6 +827,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", }, wantErr: DeviceAuthStateError(domain.DeviceAuthStateExpired), }, @@ -546,6 +859,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", }, wantErr: DeviceAuthStateError(domain.DeviceAuthStateDenied), }, @@ -583,6 +897,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", }, wantErr: DeviceAuthStateError(domain.DeviceAuthStateDone), }, @@ -646,6 +961,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", }, wantErr: zerrors.ThrowPreconditionFailed(nil, "OIDCS-kj3g2", "Errors.User.NotActive"), }, @@ -725,6 +1041,114 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", + }, + want: &OIDCSession{ + TokenID: "V2_oidcSessionID-at_accessTokenID", + ClientID: "clientID", + UserID: "userID", + Audience: []string{"audience"}, + Expiration: time.Time{}.Add(time.Hour), + Scope: []string{"openid", "offline_access"}, + AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + AuthTime: testNow, + PreferredLanguage: &language.Afrikaans, + UserAgent: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + Reason: domain.TokenReasonAuthRequest, + SessionID: "sessionID", + }, + }, + { + name: "approved with backChannelLogout (feature enabled), success", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("123", "instance1"), + "clientID", "123", "456", time.Now().Add(-time.Minute), + []string{"openid", "offline_access"}, + []string{"audience"}, false, + ), + ), + eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewApprovedEvent(ctx, + deviceauth.NewAggregate("123", "instance1"), + "userID", "org1", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + testNow, &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + ), + ), + ), + expectFilter( + user.NewHumanAddedEvent( + ctx, + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "email", + false, + ), + ), + expectFilter(), // token lifetime + expectPush( + oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"}, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "", &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + sessionlogout.NewBackChannelLogoutRegisteredEvent(context.Background(), + &sessionlogout.NewAggregate("sessionID", "instance1").Aggregate, + "V2_oidcSessionID", + "userID", + "clientID", + "backChannelLogoutURI", + ), + oidcsession.NewAccessTokenAddedEvent(context.Background(), + &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "at_accessTokenID", []string{"openid", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil, + ), + user.NewUserTokenV2AddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "at_accessTokenID"), + deviceauth.NewDoneEvent(ctx, + deviceauth.NewAggregate("123", "instance1"), + ), + ), + ), + idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID", "accessTokenID"), + defaultAccessTokenLifetime: time.Hour, + defaultRefreshTokenLifetime: 7 * 24 * time.Hour, + defaultRefreshTokenIdleLifetime: 24 * time.Hour, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + authz.WithFeatures(ctx, feature.Features{ + EnableBackChannelLogout: true, + }), + "123", + "backChannelLogoutURI", }, want: &OIDCSession{ TokenID: "V2_oidcSessionID-at_accessTokenID", @@ -825,6 +1249,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { args: args{ ctx, "123", + "", }, want: &OIDCSession{ TokenID: "V2_oidcSessionID-at_accessTokenID", @@ -858,7 +1283,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime, keyAlgorithm: tt.fields.keyAlgorithm, } - got, err := c.CreateOIDCSessionFromDeviceAuth(tt.args.ctx, tt.args.deviceCode) + got, err := c.CreateOIDCSessionFromDeviceAuth(tt.args.ctx, tt.args.deviceCode, tt.args.backChannelLogoutURI) c.jobs.Wait() require.ErrorIs(t, err, tt.wantErr) diff --git a/internal/command/existing_label_policies_model.go b/internal/command/existing_label_policies_model.go deleted file mode 100644 index dda39980f1..0000000000 --- a/internal/command/existing_label_policies_model.go +++ /dev/null @@ -1,55 +0,0 @@ -package command - -import ( - "context" - - "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/repository/org" -) - -type ExistingLabelPoliciesReadModel struct { - eventstore.WriteModel - - aggregateIDs []string -} - -func NewExistingLabelPoliciesReadModel(ctx context.Context) *ExistingLabelPoliciesReadModel { - return &ExistingLabelPoliciesReadModel{} -} - -func (rm *ExistingLabelPoliciesReadModel) AppendEvents(events ...eventstore.Event) { - rm.WriteModel.AppendEvents(events...) -} - -func (rm *ExistingLabelPoliciesReadModel) Reduce() error { - for _, event := range rm.Events { - switch e := event.(type) { - case *instance.LabelPolicyAddedEvent, - *org.LabelPolicyAddedEvent: - rm.aggregateIDs = append(rm.aggregateIDs, e.Aggregate().ID) - case *org.LabelPolicyRemovedEvent: - for i := len(rm.aggregateIDs) - 1; i >= 0; i-- { - if rm.aggregateIDs[i] == e.Aggregate().ID { - copy(rm.aggregateIDs[i:], rm.aggregateIDs[i+1:]) - rm.aggregateIDs[len(rm.aggregateIDs)-1] = "" - rm.aggregateIDs = rm.aggregateIDs[:len(rm.aggregateIDs)-1] - } - } - } - } - return rm.WriteModel.Reduce() -} - -func (rm *ExistingLabelPoliciesReadModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AddQuery(). - AggregateTypes(instance.AggregateType). - EventTypes(instance.LabelPolicyAddedEventType). - Or(). - AggregateTypes(org.AggregateType). - EventTypes( - org.LabelPolicyAddedEventType, - org.LabelPolicyRemovedEventType). - Builder() -} diff --git a/internal/command/idp.go b/internal/command/idp.go index 84e6a7ddc2..821a577900 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -20,6 +20,7 @@ type GenericOAuthProvider struct { UserEndpoint string Scopes []string IDAttribute string + UsePKCE bool IDPOptions idp.Options } @@ -30,6 +31,7 @@ type GenericOIDCProvider struct { ClientSecret string Scopes []string IsIDTokenMapping bool + UsePKCE bool IDPOptions idp.Options } @@ -107,6 +109,7 @@ type LDAPProvider struct { UserObjectClasses []string UserFilters []string Timeout time.Duration + RootCA []byte LDAPAttributes idp.LDAPAttributes IDPOptions idp.Options } diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 483cdcd08e..3cd9991679 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -25,7 +25,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, successURL, failureURL string) preparation.Validation { +func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, successURL, failureURL string, idpArguments map[string]any) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { if idpID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing") @@ -53,24 +53,25 @@ func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, s successURL, failureURL, idpID, + idpArguments, ), }, nil }, nil } } -func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureURL, resourceOwner string) (*IDPIntentWriteModel, *domain.ObjectDetails, error) { - id, err := c.idGenerator.Next() - if err != nil { - return nil, nil, err - } - writeModel := NewIDPIntentWriteModel(id, resourceOwner) - if err != nil { - return nil, nil, err +func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL, failureURL, resourceOwner string, idpArguments map[string]any) (*IDPIntentWriteModel, *domain.ObjectDetails, error) { + if intentID == "" { + var err error + intentID, err = c.idGenerator.Next() + if err != nil { + return nil, nil, err + } } + writeModel := NewIDPIntentWriteModel(intentID, resourceOwner) //nolint: staticcheck - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL)) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments)) if err != nil { return nil, nil, err } @@ -132,18 +133,17 @@ func (c *Commands) GetActiveIntent(ctx context.Context, intentID string) (*IDPIn return intent, nil } -func (c *Commands) AuthFromProvider(ctx context.Context, idpID, state string, idpCallback, samlRootURL string) (string, bool, error) { +func (c *Commands) AuthFromProvider(ctx context.Context, idpID, idpCallback, samlRootURL string) (state string, session idp.Session, err error) { + state, err = c.idGenerator.Next() + if err != nil { + return "", nil, err + } provider, err := c.GetProvider(ctx, idpID, idpCallback, samlRootURL) if err != nil { - return "", false, err + return "", nil, err } - session, err := provider.BeginAuth(ctx, state) - if err != nil { - return "", false, err - } - - content, redirect := session.GetAuth(ctx) - return content, redirect, nil + session, err = provider.BeginAuth(ctx, state) + return state, session, err } func getIDPIntentWriteModel(ctx context.Context, writeModel *IDPIntentWriteModel, filter preparation.FilterToQueryReducer) error { diff --git a/internal/command/idp_intent_model.go b/internal/command/idp_intent_model.go index 62794323e1..c6bc26ab06 100644 --- a/internal/command/idp_intent_model.go +++ b/internal/command/idp_intent_model.go @@ -12,13 +12,14 @@ import ( type IDPIntentWriteModel struct { eventstore.WriteModel - SuccessURL *url.URL - FailureURL *url.URL - IDPID string - IDPUser []byte - IDPUserID string - IDPUserName string - UserID string + SuccessURL *url.URL + FailureURL *url.URL + IDPID string + IDPArguments map[string]any + IDPUser []byte + IDPUserID string + IDPUserName string + UserID string IDPAccessToken *crypto.CryptoValue IDPIDToken string @@ -81,6 +82,7 @@ func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) { wm.SuccessURL = e.SuccessURL wm.FailureURL = e.FailureURL wm.IDPID = e.IDPID + wm.IDPArguments = e.IDPArguments wm.State = domain.IDPIntentStateStarted } diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 832e2e9902..2400b9ee35 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -39,11 +39,13 @@ func TestCommands_CreateIntent(t *testing.T) { idGenerator id.Generator } type args struct { - ctx context.Context - idpID string - successURL string - failureURL string - instanceID string + ctx context.Context + intentID string + idpID string + successURL string + failureURL string + instanceID string + idpArguments map[string]any } type res struct { intentID string @@ -182,6 +184,7 @@ func TestCommands_CreateIntent(t *testing.T) { "user", "idAttribute", nil, + true, rep_idp.Options{}, )), ), @@ -195,6 +198,7 @@ func TestCommands_CreateIntent(t *testing.T) { success, failure, "idp", + nil, ) }(), ), @@ -235,6 +239,7 @@ func TestCommands_CreateIntent(t *testing.T) { "user", "idAttribute", nil, + true, rep_idp.Options{}, )), ), @@ -248,6 +253,9 @@ func TestCommands_CreateIntent(t *testing.T) { success, failure, "idp", + map[string]interface{}{ + "verifier": "pkceOAuthVerifier", + }, ) }(), ), @@ -260,6 +268,9 @@ func TestCommands_CreateIntent(t *testing.T) { idpID: "idp", successURL: "https://success.url", failureURL: "https://failure.url", + idpArguments: map[string]interface{}{ + "verifier": "pkceOAuthVerifier", + }, }, res{ intentID: "id", @@ -288,6 +299,7 @@ func TestCommands_CreateIntent(t *testing.T) { "user", "idAttribute", nil, + true, rep_idp.Options{}, )), ), @@ -301,6 +313,9 @@ func TestCommands_CreateIntent(t *testing.T) { success, failure, "idp", + map[string]interface{}{ + "verifier": "pkceOAuthVerifier", + }, ) }(), ), @@ -313,6 +328,69 @@ func TestCommands_CreateIntent(t *testing.T) { idpID: "idp", successURL: "https://success.url", failureURL: "https://failure.url", + idpArguments: map[string]interface{}{ + "verifier": "pkceOAuthVerifier", + }, + }, + res{ + intentID: "id", + details: &domain.ObjectDetails{ResourceOwner: "instance"}, + }, + }, + { + "push, with id", + fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate, + "idp", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + "idAttribute", + nil, + true, + rep_idp.Options{}, + )), + ), + expectPush( + func() eventstore.Command { + success, _ := url.Parse("https://success.url") + failure, _ := url.Parse("https://failure.url") + return idpintent.NewStartedEvent( + context.Background(), + &idpintent.NewAggregate("id", "instance").Aggregate, + success, + failure, + "idp", + map[string]interface{}{ + "verifier": "pkceOAuthVerifier", + }, + ) + }(), + ), + ), + }, + args{ + ctx: context.Background(), + instanceID: "instance", + intentID: "id", + idpID: "idp", + successURL: "https://success.url", + failureURL: "https://failure.url", + idpArguments: map[string]interface{}{ + "verifier": "pkceOAuthVerifier", + }, }, res{ intentID: "id", @@ -326,7 +404,7 @@ func TestCommands_CreateIntent(t *testing.T) { eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } - intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.instanceID) + intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.intentID, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.instanceID, tt.args.idpArguments) require.ErrorIs(t, err, tt.res.err) if intentWriteModel != nil { assert.Equal(t, tt.res.intentID, intentWriteModel.AggregateID) @@ -342,11 +420,11 @@ func TestCommands_AuthFromProvider(t *testing.T) { type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore secretCrypto crypto.EncryptionAlgorithm + idGenerator id.Generator } type args struct { ctx context.Context idpID string - state string callbackURL string samlRootURL string } @@ -361,6 +439,22 @@ func TestCommands_AuthFromProvider(t *testing.T) { args args res res }{ + { + "error no id generator", + fields{ + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: expectEventstore(), + idGenerator: mock.NewIDGeneratorExpectError(t, zerrors.ThrowInternal(nil, "", "error id")), + }, + args{ + ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), + idpID: "idp", + callbackURL: "url", + }, + res{ + err: zerrors.ThrowInternal(nil, "", "error id"), + }, + }, { "idp not existing", fields{ @@ -368,11 +462,11 @@ func TestCommands_AuthFromProvider(t *testing.T) { eventstore: expectEventstore( expectFilter(), ), + idGenerator: mock.ExpectID(t, "id"), }, args{ ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), idpID: "idp", - state: "state", callbackURL: "url", }, res{ @@ -402,6 +496,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { "user", "idAttribute", nil, + true, rep_idp.Options{}, )), eventFromEventPusherWithInstanceID( @@ -412,11 +507,11 @@ func TestCommands_AuthFromProvider(t *testing.T) { ), ), ), + idGenerator: mock.ExpectID(t, "id"), }, args{ ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), idpID: "idp", - state: "state", callbackURL: "url", }, res{ @@ -446,6 +541,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { "user", "idAttribute", nil, + true, rep_idp.Options{}, )), ), @@ -467,19 +563,20 @@ func TestCommands_AuthFromProvider(t *testing.T) { "user", "idAttribute", nil, + true, rep_idp.Options{}, )), ), ), + idGenerator: mock.ExpectID(t, "id"), }, args{ ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), idpID: "idp", - state: "state", callbackURL: "url", }, res{ - content: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=state", + content: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=id", redirect: true, }, }, @@ -504,6 +601,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { }, []string{"openid", "profile", "User.Read"}, false, + true, rep_idp.Options{}, )), eventFromEventPusherWithInstanceID( @@ -540,6 +638,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { }, []string{"openid", "profile", "User.Read"}, false, + true, rep_idp.Options{}, )), eventFromEventPusherWithInstanceID( @@ -561,15 +660,15 @@ func TestCommands_AuthFromProvider(t *testing.T) { )), ), ), + idGenerator: mock.ExpectID(t, "id"), }, args{ ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), idpID: "idp", - state: "state", callbackURL: "url", }, res{ - content: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=state", + content: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=id", redirect: true, }, }, @@ -579,9 +678,16 @@ func TestCommands_AuthFromProvider(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.secretCrypto, + idGenerator: tt.fields.idGenerator, } - content, redirect, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL, tt.args.samlRootURL) + _, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL) require.ErrorIs(t, err, tt.res.err) + + var content string + var redirect bool + if err == nil { + content, redirect = session.GetAuth(tt.args.ctx) + } assert.Equal(t, tt.res.redirect, redirect) assert.Equal(t, tt.res.content, content) }) @@ -592,11 +698,11 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore secretCrypto crypto.EncryptionAlgorithm + idGenerator id.Generator } type args struct { ctx context.Context idpID string - state string callbackURL string samlRootURL string } @@ -669,6 +775,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { success, failure, "idp", + nil, ) }(), ), @@ -683,11 +790,11 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { }, ), ), + idGenerator: mock.ExpectID(t, "id"), }, args{ ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), idpID: "idp", - state: "id", callbackURL: "url", samlRootURL: "samlurl", }, @@ -705,10 +812,12 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.secretCrypto, + idGenerator: tt.fields.idGenerator, } - content, _, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL, tt.args.samlRootURL) + _, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL) require.ErrorIs(t, err, tt.res.err) + content, _ := session.GetAuth(tt.args.ctx) authURL, err := url.Parse(content) require.NoError(t, err) diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index fa38b4a075..5257d38bf4 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -1,6 +1,7 @@ package command import ( + "bytes" "net/http" "reflect" "slices" @@ -44,6 +45,7 @@ type OAuthIDPWriteModel struct { UserEndpoint string Scopes []string IDAttribute string + UsePKCE bool idp.Options State domain.IDPState @@ -72,6 +74,7 @@ func (wm *OAuthIDPWriteModel) reduceAddedEvent(e *idp.OAuthIDPAddedEvent) { wm.UserEndpoint = e.UserEndpoint wm.Scopes = e.Scopes wm.IDAttribute = e.IDAttribute + wm.UsePKCE = e.UsePKCE wm.Options = e.Options wm.State = domain.IDPStateActive } @@ -101,6 +104,9 @@ func (wm *OAuthIDPWriteModel) reduceChangedEvent(e *idp.OAuthIDPChangedEvent) { if e.IDAttribute != nil { wm.IDAttribute = *e.IDAttribute } + if e.UsePKCE != nil { + wm.UsePKCE = *e.UsePKCE + } wm.Options.ReduceChanges(e.OptionChanges) } @@ -114,6 +120,7 @@ func (wm *OAuthIDPWriteModel) NewChanges( userEndpoint, idAttribute string, scopes []string, + usePKCE bool, options idp.Options, ) ([]idp.OAuthIDPChanges, error) { changes := make([]idp.OAuthIDPChanges, 0) @@ -147,6 +154,9 @@ func (wm *OAuthIDPWriteModel) NewChanges( if wm.IDAttribute != idAttribute { changes = append(changes, idp.ChangeOAuthIDAttribute(idAttribute)) } + if wm.UsePKCE != usePKCE { + changes = append(changes, idp.ChangeOAuthUsePKCE(usePKCE)) + } opts := wm.Options.Changes(options) if !opts.IsZero() { changes = append(changes, idp.ChangeOAuthOptions(opts)) @@ -207,6 +217,7 @@ type OIDCIDPWriteModel struct { ClientSecret *crypto.CryptoValue Scopes []string IsIDTokenMapping bool + UsePKCE bool idp.Options State domain.IDPState @@ -247,6 +258,7 @@ func (wm *OIDCIDPWriteModel) reduceAddedEvent(e *idp.OIDCIDPAddedEvent) { wm.ClientSecret = e.ClientSecret wm.Scopes = e.Scopes wm.IsIDTokenMapping = e.IsIDTokenMapping + wm.UsePKCE = e.UsePKCE wm.Options = e.Options wm.State = domain.IDPStateActive } @@ -270,6 +282,9 @@ func (wm *OIDCIDPWriteModel) reduceChangedEvent(e *idp.OIDCIDPChangedEvent) { if e.IsIDTokenMapping != nil { wm.IsIDTokenMapping = *e.IsIDTokenMapping } + if e.UsePKCE != nil { + wm.UsePKCE = *e.UsePKCE + } wm.Options.ReduceChanges(e.OptionChanges) } @@ -280,7 +295,7 @@ func (wm *OIDCIDPWriteModel) NewChanges( clientSecretString string, secretCrypto crypto.EncryptionAlgorithm, scopes []string, - idTokenMapping bool, + idTokenMapping, usePKCE bool, options idp.Options, ) ([]idp.OIDCIDPChanges, error) { changes := make([]idp.OIDCIDPChanges, 0) @@ -308,6 +323,9 @@ func (wm *OIDCIDPWriteModel) NewChanges( if wm.IsIDTokenMapping != idTokenMapping { changes = append(changes, idp.ChangeOIDCIsIDTokenMapping(idTokenMapping)) } + if wm.UsePKCE != usePKCE { + changes = append(changes, idp.ChangeOIDCUsePKCE(usePKCE)) + } opts := wm.Options.Changes(options) if !opts.IsZero() { changes = append(changes, idp.ChangeOIDCOptions(opts)) @@ -1366,6 +1384,7 @@ type LDAPIDPWriteModel struct { UserObjectClasses []string UserFilters []string Timeout time.Duration + RootCA []byte idp.LDAPAttributes idp.Options @@ -1406,6 +1425,7 @@ func (wm *LDAPIDPWriteModel) reduceAddedEvent(e *idp.LDAPIDPAddedEvent) { wm.UserObjectClasses = e.UserObjectClasses wm.UserFilters = e.UserFilters wm.Timeout = e.Timeout + wm.RootCA = e.RootCA wm.LDAPAttributes = e.LDAPAttributes wm.Options = e.Options wm.State = domain.IDPStateActive @@ -1460,6 +1480,7 @@ func (wm *LDAPIDPWriteModel) NewChanges( userObjectClasses []string, userFilters []string, timeout time.Duration, + rootCA []byte, secretCrypto crypto.EncryptionAlgorithm, attributes idp.LDAPAttributes, options idp.Options, @@ -1501,6 +1522,9 @@ func (wm *LDAPIDPWriteModel) NewChanges( if wm.Timeout != timeout { changes = append(changes, idp.ChangeLDAPTimeout(timeout)) } + if !bytes.Equal(wm.RootCA, rootCA) { + changes = append(changes, idp.ChangeLDAPRootCA(rootCA)) + } attrs := wm.LDAPAttributes.Changes(attributes) if !attrs.IsZero() { changes = append(changes, idp.ChangeLDAPAttributes(attrs)) @@ -1582,6 +1606,7 @@ func (wm *LDAPIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp wm.UserObjectClasses, wm.UserFilters, wm.Timeout, + wm.RootCA, callbackURL, opts..., ), nil diff --git a/internal/command/instance.go b/internal/command/instance.go index 99075ccfad..1080168842 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -233,21 +233,25 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str return "", "", nil, nil, err } - events, err := c.eventstore.Push(ctx, cmds...) + _, err = c.eventstore.Push(ctx, cmds...) if err != nil { return "", "", nil, nil, err } + // RolePermissions need to be pushed in separate transaction. + // https://github.com/zitadel/zitadel/issues/9293 + details, err := c.SynchronizeRolePermission(ctx, setup.zitadel.instanceID, setup.RolePermissionMappings) + if err != nil { + return "", "", nil, nil, err + } + details.ResourceOwner = setup.zitadel.orgID + var token string if pat != nil { token = pat.Token } - return setup.zitadel.instanceID, token, machineKey, &domain.ObjectDetails{ - Sequence: events[len(events)-1].Sequence(), - EventDate: events[len(events)-1].CreatedAt(), - ResourceOwner: setup.zitadel.orgID, - }, nil + return setup.zitadel.instanceID, token, machineKey, details, nil } func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string) context.Context { @@ -380,7 +384,6 @@ func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup setup.LabelPolicy.ThemeMode, ), prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate), - prepareAddRolePermissions(instanceAgg, setup.RolePermissionMappings), } } @@ -848,9 +851,6 @@ func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLangu if err := domain.LanguageIsAllowed(false, restrictionsWM.allowedLanguages, defaultLanguage); err != nil { return nil, err } - if err != nil { - return nil, err - } return []eventstore.Command{instance.NewDefaultLanguageSetEvent(ctx, &a.Aggregate, defaultLanguage)}, nil }, nil } diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index 1f714671bd..cb12bff828 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -21,7 +21,6 @@ type InstanceFeatures struct { LegacyIntrospection *bool UserSchema *bool TokenExchange *bool - Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType WebKey *bool DebugOIDCParentError *bool @@ -30,6 +29,7 @@ type InstanceFeatures struct { EnableBackChannelLogout *bool LoginV2 *feature.LoginV2 PermissionCheckV2 *bool + ConsoleUseV2UserApi *bool } func (m *InstanceFeatures) isEmpty() bool { @@ -38,7 +38,6 @@ func (m *InstanceFeatures) isEmpty() bool { m.LegacyIntrospection == nil && m.UserSchema == nil && m.TokenExchange == nil && - m.Actions == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && m.WebKey == nil && @@ -47,7 +46,7 @@ func (m *InstanceFeatures) isEmpty() bool { m.DisableUserTokenEvent == nil && m.EnableBackChannelLogout == nil && m.LoginV2 == nil && - m.PermissionCheckV2 == nil + m.PermissionCheckV2 == nil && m.ConsoleUseV2UserApi == nil } func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) { diff --git a/internal/command/instance_features_model.go b/internal/command/instance_features_model.go index aaa8b2e53a..977a46b6c2 100644 --- a/internal/command/instance_features_model.go +++ b/internal/command/instance_features_model.go @@ -71,7 +71,6 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceLegacyIntrospectionEventType, feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, - feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, @@ -80,6 +79,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceEnableBackChannelLogout, feature_v2.InstanceLoginVersion, feature_v2.InstancePermissionCheckV2, + feature_v2.InstanceConsoleUseV2UserApi, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -107,9 +107,6 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyUserSchema: v := value.(bool) features.UserSchema = &v - case feature.KeyActions: - v := value.(bool) - features.Actions = &v case feature.KeyImprovedPerformance: v := value.([]feature.ImprovedPerformanceType) features.ImprovedPerformance = v @@ -133,6 +130,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyPermissionCheckV2: v := value.(bool) features.PermissionCheckV2 = &v + case feature.KeyConsoleUseV2UserApi: + v := value.(bool) + features.ConsoleUseV2UserApi = &v } } @@ -144,7 +144,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.InstanceLegacyIntrospectionEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.InstanceTokenExchangeEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.InstanceActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType) @@ -153,5 +152,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.InstanceEnableBackChannelLogout) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginV2, f.LoginV2, feature_v2.InstanceLoginVersion) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.PermissionCheckV2, f.PermissionCheckV2, feature_v2.InstancePermissionCheckV2) + cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.ConsoleUseV2UserApi, f.ConsoleUseV2UserApi, feature_v2.InstanceConsoleUseV2UserApi) return cmds } diff --git a/internal/command/instance_features_test.go b/internal/command/instance_features_test.go index e6b6bb4346..02e8896a0c 100644 --- a/internal/command/instance_features_test.go +++ b/internal/command/instance_features_test.go @@ -149,24 +149,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ResourceOwner: "instance1", }, }, - { - name: "set Actions", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, true, - ), - ), - ), - args: args{ctx, &InstanceFeatures{ - Actions: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "instance1", - }, - }, { name: "push error", eventstore: expectEventstore( @@ -204,10 +186,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, true, - ), feature_v2.NewSetEvent[bool]( ctx, aggregate, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, true, @@ -219,7 +197,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 99ab506424..348f55cd9c 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -2,6 +2,7 @@ package command import ( "context" + "crypto/x509" "strings" "github.com/zitadel/saml/pkg/provider/xml" @@ -656,6 +657,7 @@ func (c *Commands) prepareAddInstanceOAuthProvider(a *instance.Aggregate, writeM provider.UserEndpoint, provider.IDAttribute, provider.Scopes, + provider.UsePKCE, provider.IDPOptions, ), }, nil @@ -711,6 +713,7 @@ func (c *Commands) prepareUpdateInstanceOAuthProvider(a *instance.Aggregate, wri provider.UserEndpoint, provider.IDAttribute, provider.Scopes, + provider.UsePKCE, provider.IDPOptions, ) if err != nil || event == nil { @@ -759,6 +762,7 @@ func (c *Commands) prepareAddInstanceOIDCProvider(a *instance.Aggregate, writeMo secret, provider.Scopes, provider.IsIDTokenMapping, + provider.UsePKCE, provider.IDPOptions, ), }, nil @@ -803,6 +807,7 @@ func (c *Commands) prepareUpdateInstanceOIDCProvider(a *instance.Aggregate, writ c.idpConfigEncryption, provider.Scopes, provider.IsIDTokenMapping, + provider.UsePKCE, provider.IDPOptions, ) if err != nil || event == nil { @@ -1528,6 +1533,12 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, writeMo if len(provider.UserFilters) == 0 { return nil, zerrors.ThrowInvalidArgument(nil, "INST-aAx905n", "Errors.Invalid.Argument") } + if len(provider.RootCA) > 0 { + if err := validateRootCA(provider.RootCA); err != nil { + return nil, err + } + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -1556,6 +1567,7 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, writeMo provider.UserObjectClasses, provider.UserFilters, provider.Timeout, + provider.RootCA, provider.LDAPAttributes, provider.IDPOptions, ), @@ -1564,6 +1576,14 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, writeMo } } +func validateRootCA(pemCerts []byte) error { + rootCAs := x509.NewCertPool() + if ok := rootCAs.AppendCertsFromPEM(pemCerts); !ok { + return zerrors.ThrowInvalidArgument(nil, "INST-cwqVVdBwKt", "Errors.Invalid.Argument") + } + return nil +} + func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writeModel *InstanceLDAPIDPWriteModel, provider LDAPProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { @@ -1590,6 +1610,11 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writ if len(provider.UserFilters) == 0 { return nil, zerrors.ThrowInvalidArgument(nil, "INST-aAx901n", "Errors.Invalid.Argument") } + if len(provider.RootCA) > 0 { + if err := validateRootCA(provider.RootCA); err != nil { + return nil, err + } + } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -1616,6 +1641,7 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writ provider.UserObjectClasses, provider.UserFilters, provider.Timeout, + provider.RootCA, c.idpConfigEncryption, provider.LDAPAttributes, provider.IDPOptions, diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index 5c895b1fc7..d94c19d318 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -68,6 +68,7 @@ func (wm *InstanceOAuthIDPWriteModel) NewChangedEvent( userEndpoint, idAttribute string, scopes []string, + usePKCE bool, options idp.Options, ) (*instance.OAuthIDPChangedEvent, error) { @@ -81,6 +82,7 @@ func (wm *InstanceOAuthIDPWriteModel) NewChangedEvent( userEndpoint, idAttribute, scopes, + usePKCE, options, ) if err != nil || len(changes) == 0 { @@ -174,7 +176,7 @@ func (wm *InstanceOIDCIDPWriteModel) NewChangedEvent( clientSecretString string, secretCrypto crypto.EncryptionAlgorithm, scopes []string, - idTokenMapping bool, + idTokenMapping, usePKCE bool, options idp.Options, ) (*instance.OIDCIDPChangedEvent, error) { @@ -186,6 +188,7 @@ func (wm *InstanceOIDCIDPWriteModel) NewChangedEvent( secretCrypto, scopes, idTokenMapping, + usePKCE, options, ) if err != nil || len(changes) == 0 { @@ -768,6 +771,7 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent( userObjectClasses []string, userFilters []string, timeout time.Duration, + rootCA []byte, secretCrypto crypto.EncryptionAlgorithm, attributes idp.LDAPAttributes, options idp.Options, @@ -784,6 +788,7 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent( userObjectClasses, userFilters, timeout, + rootCA, secretCrypto, attributes, options, diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index 22defda532..7002598af5 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -87,6 +87,26 @@ var ( `) + validLDAPRootCA = []byte(`-----BEGIN CERTIFICATE----- +MIIDITCCAgmgAwIBAgIUKjAUmxsHO44X+/TKBNciPgNl1GEwDQYJKoZIhvcNAQEL +BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTI0MTIxOTEz +Mzc1MVoXDTI1MTIxOTEzMzc1MVowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0QYuJsayILRI +hVT7G1DlitVSXnt1iw3gEXJZfe81Egz06fUbvXF6Yo1LJmwYpqe/rm+hf4FNUb8e +2O+LH2FieA9FkVe4P2gKOzw87A/KxvpV8stgNgl4LlqRCokbc1AzeE/NiLr5TcTD +RXm3DUcYxXxinprtDu2jftFysaOZmNAukvE/iL6qS3X6ggVEDDM7tY9n5FV2eJ4E +p0ImKfypi2aZYROxOK+v5x9ryFRMl4y07lMDvmtcV45uXYmfGNCgG9PNf91Kk/mh +JxEQbxycJwFoSi9XWljR8ahPdO11LXG7Dsj/RVbY8k2LdKNstl6Ae3aCpbe9u2Pj +vxYs1bVJuQIDAQABo1MwUTAdBgNVHQ4EFgQU+mRVN5HYJWgnpopReaLhf2cMcoYw +HwYDVR0jBBgwFoAU+mRVN5HYJWgnpopReaLhf2cMcoYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEABJpHVuc9tGhD04infRVlofvqXIUizTlOrjZX +vozW9pIhSWEHX8o+sJP8AMZLnrsdq+bm0HE0HvgYrw7Lb8pd4FpR46TkFHjeukoj +izqfgckjIBl2nwPGlynbKA0/U/rTCSxVt7XiAn+lgYUGIpOzNdk06/hRMitrMNB7 +t2C97NseVC4b1ZgyFrozsefCfUmD8IJF0+XJ4Wzmsh0jRrI8koCtVmPYnKn6vw1b +cZprg/97CWHYrsavd406wOB60CMtYl83Q16ucOF1dretDFqJC5kY+aFLvuqfag2+ +kIaoPV1MnGsxveQyyHdOsEatS5XOv/1OWcmnvePDPxcvb9jCcw== +-----END CERTIFICATE----- +`) ) func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) { @@ -270,6 +290,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) { "user", "idAttribute", nil, + true, idp.Options{}, ), ), @@ -287,6 +308,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) { TokenEndpoint: "token", UserEndpoint: "user", IDAttribute: "idAttribute", + UsePKCE: true, }, }, res: res{ @@ -315,6 +337,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) { "user", "idAttribute", []string{"user"}, + true, idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -338,6 +361,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) { UserEndpoint: "user", Scopes: []string{"user"}, IDAttribute: "idAttribute", + UsePKCE: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -569,6 +593,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) { "user", "idAttribute", nil, + true, idp.Options{}, )), ), @@ -584,6 +609,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) { TokenEndpoint: "token", UserEndpoint: "user", IDAttribute: "idAttribute", + UsePKCE: true, }, }, res: res{ @@ -611,6 +637,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) { "user", "idAttribute", nil, + false, idp.Options{}, )), ), @@ -633,6 +660,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) { idp.ChangeOAuthUserEndpoint("new user"), idp.ChangeOAuthScopes([]string{"openid", "profile"}), idp.ChangeOAuthIDAttribute("newAttribute"), + idp.ChangeOAuthUsePKCE(true), idp.ChangeOAuthOptions(idp.OptionChanges{ IsCreationAllowed: &t, IsLinkingAllowed: &t, @@ -659,6 +687,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) { UserEndpoint: "new user", Scopes: []string{"openid", "profile"}, IDAttribute: "newAttribute", + UsePKCE: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -805,6 +834,7 @@ func TestCommandSide_AddInstanceGenericOIDCIDP(t *testing.T) { }, nil, false, + true, idp.Options{}, ), ), @@ -819,6 +849,7 @@ func TestCommandSide_AddInstanceGenericOIDCIDP(t *testing.T) { Issuer: "issuer", ClientID: "clientID", ClientSecret: "clientSecret", + UsePKCE: true, }, }, res: res{ @@ -845,6 +876,7 @@ func TestCommandSide_AddInstanceGenericOIDCIDP(t *testing.T) { }, []string{openid.ScopeOpenID}, true, + true, idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -866,6 +898,7 @@ func TestCommandSide_AddInstanceGenericOIDCIDP(t *testing.T) { ClientSecret: "clientSecret", Scopes: []string{openid.ScopeOpenID}, IsIDTokenMapping: true, + UsePKCE: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -1029,6 +1062,7 @@ func TestCommandSide_UpdateInstanceGenericOIDCIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1066,6 +1100,7 @@ func TestCommandSide_UpdateInstanceGenericOIDCIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1086,6 +1121,7 @@ func TestCommandSide_UpdateInstanceGenericOIDCIDP(t *testing.T) { }), idp.ChangeOIDCScopes([]string{"openid", "profile"}), idp.ChangeOIDCIsIDTokenMapping(true), + idp.ChangeOIDCUsePKCE(true), idp.ChangeOIDCOptions(idp.OptionChanges{ IsCreationAllowed: &t, IsLinkingAllowed: &t, @@ -1110,6 +1146,7 @@ func TestCommandSide_UpdateInstanceGenericOIDCIDP(t *testing.T) { ClientSecret: "newSecret", Scopes: []string{"openid", "profile"}, IsIDTokenMapping: true, + UsePKCE: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -1253,6 +1290,7 @@ func TestCommandSide_MigrateInstanceGenericOIDCToAzureADProvider(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1311,6 +1349,7 @@ func TestCommandSide_MigrateInstanceGenericOIDCToAzureADProvider(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1475,6 +1514,7 @@ func TestCommandSide_MigrateInstanceOIDCToGoogleIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1527,6 +1567,7 @@ func TestCommandSide_MigrateInstanceOIDCToGoogleIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -4237,6 +4278,34 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { }, }, }, + { + "invalid rootCA", + fields{ + eventstore: expectEventstore(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + StartTLS: false, + BaseDN: "baseDN", + BindDN: "dn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Second * 30, + RootCA: []byte("certificate"), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-cwqVVdBwKt", "Errors.Invalid.Argument")) + }, + }, + }, { name: "ok", fields: fields{ @@ -4260,6 +4329,7 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { []string{"object"}, []string{"filter"}, time.Second*30, + nil, idp.LDAPAttributes{}, idp.Options{}, ), @@ -4311,6 +4381,7 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { []string{"object"}, []string{"filter"}, time.Second*30, + validLDAPRootCA, idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "firstName", @@ -4351,6 +4422,7 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { UserObjectClasses: []string{"object"}, UserFilters: []string{"filter"}, Timeout: time.Second * 30, + RootCA: validLDAPRootCA, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "firstName", @@ -4576,6 +4648,32 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { }, }, }, + { + "invalid rootCA", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BaseDN: "baseDN", + BindDN: "binddn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + RootCA: []byte("certificate"), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-cwqVVdBwKt", "Errors.Invalid.Argument")) + }, + }, + }, { name: "not found", fields: fields{ @@ -4626,6 +4724,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { []string{"object"}, []string{"filter"}, time.Second*30, + validLDAPRootCA, idp.LDAPAttributes{}, idp.Options{}, )), @@ -4645,6 +4744,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { UserObjectClasses: []string{"object"}, UserFilters: []string{"filter"}, Timeout: time.Second * 30, + RootCA: validLDAPRootCA, }, }, res: res{ @@ -4674,6 +4774,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { []string{"object"}, []string{"filter"}, time.Second*30, + nil, idp.LDAPAttributes{}, idp.Options{}, )), @@ -4720,6 +4821,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { IsAutoCreation: &t, IsAutoUpdate: &t, }), + idp.ChangeLDAPRootCA(validLDAPRootCA), }, ) return event @@ -4742,6 +4844,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { UserObjectClasses: []string{"new object"}, UserFilters: []string{"new filter"}, Timeout: time.Second * 20, + RootCA: validLDAPRootCA, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "new id", FirstNameAttribute: "new firstName", diff --git a/internal/command/instance_permissions.go b/internal/command/instance_permissions.go deleted file mode 100644 index c46c8f7c4a..0000000000 --- a/internal/command/instance_permissions.go +++ /dev/null @@ -1,29 +0,0 @@ -package command - -import ( - "context" - "strings" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/command/preparation" - "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/repository/permission" -) - -func prepareAddRolePermissions(a *instance.Aggregate, roles []authz.RoleMapping) preparation.Validation { - return func() (preparation.CreateCommands, error) { - return func(ctx context.Context, _ preparation.FilterToQueryReducer) (cmds []eventstore.Command, _ error) { - aggregate := permission.NewAggregate(a.InstanceID) - for _, r := range roles { - if strings.HasPrefix(r.Role, "SYSTEM") { - continue - } - for _, p := range r.Permissions { - cmds = append(cmds, permission.NewAddedEvent(ctx, aggregate, r.Role, p)) - } - } - return cmds, nil - }, nil - } -} diff --git a/internal/command/instance_role_permissions.go b/internal/command/instance_role_permissions.go new file mode 100644 index 0000000000..fce272cc12 --- /dev/null +++ b/internal/command/instance_role_permissions.go @@ -0,0 +1,91 @@ +package command + +import ( + "context" + "database/sql" + _ "embed" + "strings" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/permission" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +// SynchronizeRolePermission checks the current state of role permissions in the eventstore for the aggregate. +// It pushes the commands required to reach the desired state passed in target. +// For system level permissions aggregateID must be set to `SYSTEM`, else it is the instance ID. +func (c *Commands) SynchronizeRolePermission(ctx context.Context, aggregateID string, target []authz.RoleMapping) (_ *domain.ObjectDetails, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + cmds, err := synchronizeRolePermissionCommands(ctx, c.eventstore.Client(), aggregateID, + rolePermissionMappingsToDatabaseMap(target, aggregateID == "SYSTEM"), + ) + if err != nil { + return nil, zerrors.ThrowInternal(err, "COMMA-Iej2r", "Errors.Internal") + } + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + logging.WithError(err).Error("failed to push role permission commands") + return nil, zerrors.ThrowInternal(err, "COMMA-AiV3u", "Errors.Internal") + } + return pushedEventsToObjectDetails(events), nil +} + +func rolePermissionMappingsToDatabaseMap(mappings []authz.RoleMapping, system bool) database.Map[[]string] { + out := make(database.Map[[]string], len(mappings)) + for _, m := range mappings { + if system == strings.HasPrefix(m.Role, "SYSTEM") { + out[m.Role] = m.Permissions + } + } + return out +} + +var ( + //go:embed instance_role_permissions_sync.sql + instanceRolePermissionsSyncQuery string +) + +// synchronizeRolePermissionCommands checks the current state of role permissions in the eventstore for the aggregate. +// It returns the commands required to reach the desired state passed in target. +// For system level permissions aggregateID must be set to `SYSTEM`, else it is the instance ID. +func synchronizeRolePermissionCommands(ctx context.Context, db *database.DB, aggregateID string, target database.Map[[]string]) (cmds []eventstore.Command, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = db.QueryContext(ctx, + rolePermissionScanner(ctx, permission.NewAggregate(aggregateID), &cmds), + instanceRolePermissionsSyncQuery, + aggregateID, target) + if err != nil { + return nil, err + } + return cmds, nil +} + +func rolePermissionScanner(ctx context.Context, aggregate *eventstore.Aggregate, cmds *[]eventstore.Command) func(rows *sql.Rows) error { + return func(rows *sql.Rows) error { + for rows.Next() { + var operation, role, perm string + if err := rows.Scan(&operation, &role, &perm); err != nil { + return err + } + logging.WithFields("aggregate_id", aggregate.ID, "operation", operation, "role", role, "permission", perm).Debug("sync role permission") + switch operation { + case "add": + *cmds = append(*cmds, permission.NewAddedEvent(ctx, aggregate, role, perm)) + case "remove": + *cmds = append(*cmds, permission.NewRemovedEvent(ctx, aggregate, role, perm)) + } + } + return rows.Close() + + } +} diff --git a/cmd/setup/sync_role_permissions.sql b/internal/command/instance_role_permissions_sync.sql similarity index 100% rename from cmd/setup/sync_role_permissions.sql rename to internal/command/instance_role_permissions_sync.sql diff --git a/internal/command/instance_role_permissions_test.go b/internal/command/instance_role_permissions_test.go new file mode 100644 index 0000000000..0a8d84b9e7 --- /dev/null +++ b/internal/command/instance_role_permissions_test.go @@ -0,0 +1,139 @@ +package command + +import ( + "context" + "database/sql" + "database/sql/driver" + _ "embed" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/database/mock" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/permission" +) + +func Test_rolePermissionMappingsToDatabaseMap(t *testing.T) { + type args struct { + mappings []authz.RoleMapping + system bool + } + tests := []struct { + name string + args args + want database.Map[[]string] + }{ + { + name: "instance", + args: args{ + mappings: []authz.RoleMapping{ + {Role: "role1", Permissions: []string{"permission1", "permission2"}}, + {Role: "role2", Permissions: []string{"permission3", "permission4"}}, + {Role: "SYSTEM_ROLE", Permissions: []string{"permission5", "permission6"}}, + }, + system: false, + }, + want: database.Map[[]string]{ + "role1": []string{"permission1", "permission2"}, + "role2": []string{"permission3", "permission4"}, + }, + }, + { + name: "system", + args: args{ + mappings: []authz.RoleMapping{ + {Role: "role1", Permissions: []string{"permission1", "permission2"}}, + {Role: "role2", Permissions: []string{"permission3", "permission4"}}, + {Role: "SYSTEM_ROLE", Permissions: []string{"permission5", "permission6"}}, + }, + system: true, + }, + want: database.Map[[]string]{ + "SYSTEM_ROLE": []string{"permission5", "permission6"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := rolePermissionMappingsToDatabaseMap(tt.args.mappings, tt.args.system) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_synchronizeRolePermissionCommands(t *testing.T) { + const aggregateID = "aggregateID" + aggregate := permission.NewAggregate(aggregateID) + target := database.Map[[]string]{ + "role1": []string{"permission1", "permission2"}, + "role2": []string{"permission3", "permission4"}, + } + tests := []struct { + name string + mock func(*testing.T) *mock.SQLMock + wantCmds []eventstore.Command + wantErr error + }{ + { + name: "query error", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectQuery(instanceRolePermissionsSyncQuery, + mock.WithQueryArgs(aggregateID, target), + mock.WithQueryErr(sql.ErrConnDone), + ), + ) + }, + wantErr: sql.ErrConnDone, + }, + { + name: "no rows", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectQuery(instanceRolePermissionsSyncQuery, + mock.WithQueryArgs(aggregateID, target), + mock.WithQueryResult([]string{"operation", "role", "permission"}, [][]driver.Value{}), + ), + ) + }, + }, + { + name: "add and remove operations", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectQuery(instanceRolePermissionsSyncQuery, + mock.WithQueryArgs(aggregateID, target), + mock.WithQueryResult([]string{"operation", "role", "permission"}, [][]driver.Value{ + {"add", "role1", "permission1"}, + {"add", "role1", "permission2"}, + {"remove", "role3", "permission5"}, + {"remove", "role3", "permission6"}, + }), + ), + ) + }, + wantCmds: []eventstore.Command{ + permission.NewAddedEvent(context.Background(), aggregate, "role1", "permission1"), + permission.NewAddedEvent(context.Background(), aggregate, "role1", "permission2"), + permission.NewRemovedEvent(context.Background(), aggregate, "role3", "permission5"), + permission.NewRemovedEvent(context.Background(), aggregate, "role3", "permission6"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := tt.mock(t) + defer mock.Assert(t) + db := &database.DB{ + DB: mock.DB, + } + gotCmds, err := synchronizeRolePermissionCommands(context.Background(), db, aggregateID, target) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantCmds, gotCmds) + }) + } +} diff --git a/internal/command/notification.go b/internal/command/notification.go deleted file mode 100644 index b0524afa89..0000000000 --- a/internal/command/notification.go +++ /dev/null @@ -1,162 +0,0 @@ -package command - -import ( - "context" - "database/sql" - "time" - - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/repository/notification" -) - -type NotificationRequest struct { - UserID string - UserResourceOwner string - TriggerOrigin string - URLTemplate string - Code *crypto.CryptoValue - CodeExpiry time.Duration - EventType eventstore.EventType - NotificationType domain.NotificationType - MessageType string - UnverifiedNotificationChannel bool - Args *domain.NotificationArguments - AggregateID string - AggregateResourceOwner string - IsOTP bool - RequiresPreviousDomain bool -} - -type NotificationRetryRequest struct { - NotificationRequest - BackOff time.Duration - NotifyUser *query.NotifyUser -} - -func NewNotificationRequest( - userID, resourceOwner, triggerOrigin string, - eventType eventstore.EventType, - notificationType domain.NotificationType, - messageType string, -) *NotificationRequest { - return &NotificationRequest{ - UserID: userID, - UserResourceOwner: resourceOwner, - TriggerOrigin: triggerOrigin, - EventType: eventType, - NotificationType: notificationType, - MessageType: messageType, - } -} - -func (r *NotificationRequest) WithCode(code *crypto.CryptoValue, expiry time.Duration) *NotificationRequest { - r.Code = code - r.CodeExpiry = expiry - return r -} - -func (r *NotificationRequest) WithURLTemplate(urlTemplate string) *NotificationRequest { - r.URLTemplate = urlTemplate - return r -} - -func (r *NotificationRequest) WithUnverifiedChannel() *NotificationRequest { - r.UnverifiedNotificationChannel = true - return r -} - -func (r *NotificationRequest) WithArgs(args *domain.NotificationArguments) *NotificationRequest { - r.Args = args - return r -} - -func (r *NotificationRequest) WithAggregate(id, resourceOwner string) *NotificationRequest { - r.AggregateID = id - r.AggregateResourceOwner = resourceOwner - return r -} - -func (r *NotificationRequest) WithOTP() *NotificationRequest { - r.IsOTP = true - return r -} - -func (r *NotificationRequest) WithPreviousDomain() *NotificationRequest { - r.RequiresPreviousDomain = true - return r -} - -// RequestNotification writes a new notification.RequestEvent with the notification.Aggregate to the eventstore -func (c *Commands) RequestNotification( - ctx context.Context, - resourceOwner string, - request *NotificationRequest, -) error { - id, err := c.idGenerator.Next() - if err != nil { - return err - } - _, err = c.eventstore.Push(ctx, notification.NewRequestedEvent(ctx, ¬ification.NewAggregate(id, resourceOwner).Aggregate, - request.UserID, - request.UserResourceOwner, - request.AggregateID, - request.AggregateResourceOwner, - request.TriggerOrigin, - request.URLTemplate, - request.Code, - request.CodeExpiry, - request.EventType, - request.NotificationType, - request.MessageType, - request.UnverifiedNotificationChannel, - request.IsOTP, - request.RequiresPreviousDomain, - request.Args)) - return err -} - -// NotificationCanceled writes a new notification.CanceledEvent with the notification.Aggregate to the eventstore -func (c *Commands) NotificationCanceled(ctx context.Context, tx *sql.Tx, id, resourceOwner string, requestError error) error { - var errorMessage string - if requestError != nil { - errorMessage = requestError.Error() - } - _, err := c.eventstore.PushWithClient(ctx, tx, notification.NewCanceledEvent(ctx, ¬ification.NewAggregate(id, resourceOwner).Aggregate, errorMessage)) - return err -} - -// NotificationSent writes a new notification.SentEvent with the notification.Aggregate to the eventstore -func (c *Commands) NotificationSent(ctx context.Context, tx *sql.Tx, id, resourceOwner string) error { - _, err := c.eventstore.PushWithClient(ctx, tx, notification.NewSentEvent(ctx, ¬ification.NewAggregate(id, resourceOwner).Aggregate)) - return err -} - -// NotificationRetryRequested writes a new notification.RetryRequestEvent with the notification.Aggregate to the eventstore -func (c *Commands) NotificationRetryRequested(ctx context.Context, tx *sql.Tx, id, resourceOwner string, request *NotificationRetryRequest, requestError error) error { - var errorMessage string - if requestError != nil { - errorMessage = requestError.Error() - } - _, err := c.eventstore.PushWithClient(ctx, tx, notification.NewRetryRequestedEvent(ctx, ¬ification.NewAggregate(id, resourceOwner).Aggregate, - request.UserID, - request.UserResourceOwner, - request.AggregateID, - request.AggregateResourceOwner, - request.TriggerOrigin, - request.URLTemplate, - request.Code, - request.CodeExpiry, - request.EventType, - request.NotificationType, - request.MessageType, - request.UnverifiedNotificationChannel, - request.IsOTP, - request.Args, - request.NotifyUser, - request.BackOff, - errorMessage)) - return err -} diff --git a/internal/command/oidc_session.go b/internal/command/oidc_session.go index bea17986ea..492d89bc2d 100644 --- a/internal/command/oidc_session.go +++ b/internal/command/oidc_session.go @@ -55,7 +55,13 @@ type AuthRequestComplianceChecker func(context.Context, *AuthRequestWriteModel) // CreateOIDCSessionFromAuthRequest creates a new OIDC Session, creates an access token and refresh token. // It returns the access token id, expiration and the refresh token. // If the underlying [AuthRequest] is a OIDC Auth Code Flow, it will set the code as exchanged. -func (c *Commands) CreateOIDCSessionFromAuthRequest(ctx context.Context, authReqId string, complianceCheck AuthRequestComplianceChecker, needRefreshToken bool) (session *OIDCSession, state string, err error) { +func (c *Commands) CreateOIDCSessionFromAuthRequest( + ctx context.Context, + authReqId string, + complianceCheck AuthRequestComplianceChecker, + needRefreshToken bool, + backChannelLogoutURI string, +) (session *OIDCSession, state string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -108,6 +114,7 @@ func (c *Commands) CreateOIDCSessionFromAuthRequest(ctx context.Context, authReq sessionModel.PreferredLanguage, sessionModel.UserAgent, ) + cmd.RegisterLogout(ctx, sessionModel.AggregateID, sessionModel.UserID, authReqModel.ClientID, backChannelLogoutURI) if authReqModel.ResponseType != domain.OIDCResponseTypeIDToken { if err = cmd.AddAccessToken(ctx, authReqModel.Scope, sessionModel.UserID, sessionModel.UserResourceOwner, domain.TokenReasonAuthRequest, nil); err != nil { diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index 18a115eb00..564c39460b 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -49,10 +49,11 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { keyAlgorithm crypto.EncryptionAlgorithm } type args struct { - ctx context.Context - authRequestID string - complianceCheck AuthRequestComplianceChecker - needRefreshToken bool + ctx context.Context + authRequestID string + complianceCheck AuthRequestComplianceChecker + needRefreshToken bool + backChannelLogoutURI string } type res struct { session *OIDCSession @@ -137,6 +138,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -181,6 +183,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -233,6 +236,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -330,6 +334,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -438,6 +443,152 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { state: "state", }, }, + { + "add successful, backChannelLogout (feature enabled)", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authrequest.NewAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate, + "loginClient", + "clientID", + "redirectURI", + "state", + "nonce", + []string{"openid", "offline_access"}, + []string{"audience"}, + domain.OIDCResponseTypeCode, + domain.OIDCResponseModeQuery, + &domain.OIDCCodeChallenge{ + Challenge: "challenge", + Method: domain.CodeChallengeMethodS256, + }, + []domain.Prompt{domain.PromptNone}, + []string{"en", "de"}, + gu.Ptr(time.Duration(0)), + gu.Ptr("loginHint"), + gu.Ptr("hintUserID"), + true, + "issuer", + ), + ), + eventFromEventPusher( + authrequest.NewCodeAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate), + ), + eventFromEventPusher( + authrequest.NewSessionLinkedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate, + "sessionID", + "userID", + testNow, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + eventFromEventPusher( + session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, + "userID", "org1", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, + testNow), + ), + ), + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + expectFilter(), // token lifetime + expectPush( + authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate), + oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"}, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + sessionlogout.NewBackChannelLogoutRegisteredEvent(context.Background(), + &sessionlogout.NewAggregate("sessionID", "instanceID").Aggregate, + "V2_oidcSessionID", + "userID", + "clientID", + "backChannelLogoutURI", + ), + oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "at_accessTokenID", []string{"openid", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil), + user.NewUserTokenV2AddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "at_accessTokenID"), + oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour), + authrequest.NewSucceededEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate), + ), + ), + idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID", "accessTokenID", "refreshTokenID"), + defaultAccessTokenLifetime: time.Hour, + defaultRefreshTokenLifetime: 7 * 24 * time.Hour, + defaultRefreshTokenIdleLifetime: 24 * time.Hour, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + + ctx: authz.WithFeatures(authz.WithInstanceID(context.Background(), "instanceID"), feature.Features{ + EnableBackChannelLogout: true, + }), + authRequestID: "V2_authRequestID", + complianceCheck: mockAuthRequestComplianceChecker(nil), + needRefreshToken: true, + backChannelLogoutURI: "backChannelLogoutURI", + }, + res{ + session: &OIDCSession{ + SessionID: "sessionID", + TokenID: "V2_oidcSessionID-at_accessTokenID", + ClientID: "clientID", + UserID: "userID", + Audience: []string{"audience"}, + Expiration: time.Time{}.Add(time.Hour), + Scope: []string{"openid", "offline_access"}, + AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + AuthTime: testNow, + Nonce: "nonce", + PreferredLanguage: &language.Afrikaans, + UserAgent: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + Reason: domain.TokenReasonAuthRequest, + RefreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID-rt_refreshTokenID:userID + }, + state: "state", + }, + }, { "disable user token event", fields{ @@ -464,6 +615,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -602,6 +754,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), false, + "issuer", ), ), eventFromEventPusher( @@ -708,7 +861,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { keyAlgorithm: tt.fields.keyAlgorithm, } c.setMilestonesCompletedForTest("instanceID") - gotSession, gotState, err := c.CreateOIDCSessionFromAuthRequest(tt.args.ctx, tt.args.authRequestID, tt.args.complianceCheck, tt.args.needRefreshToken) + gotSession, gotState, err := c.CreateOIDCSessionFromAuthRequest(tt.args.ctx, tt.args.authRequestID, tt.args.complianceCheck, tt.args.needRefreshToken, tt.args.backChannelLogoutURI) require.ErrorIs(t, err, tt.res.err) if gotSession != nil { diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index 26690b3a66..b72fc1fd77 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -628,6 +628,7 @@ func (c *Commands) prepareAddOrgOAuthProvider(a *org.Aggregate, writeModel *OrgO provider.UserEndpoint, provider.IDAttribute, provider.Scopes, + provider.UsePKCE, provider.IDPOptions, ), }, nil @@ -683,6 +684,7 @@ func (c *Commands) prepareUpdateOrgOAuthProvider(a *org.Aggregate, writeModel *O provider.UserEndpoint, provider.IDAttribute, provider.Scopes, + provider.UsePKCE, provider.IDPOptions, ) if err != nil || event == nil { @@ -731,6 +733,7 @@ func (c *Commands) prepareAddOrgOIDCProvider(a *org.Aggregate, writeModel *OrgOI secret, provider.Scopes, provider.IsIDTokenMapping, + provider.UsePKCE, provider.IDPOptions, ), }, nil @@ -775,6 +778,7 @@ func (c *Commands) prepareUpdateOrgOIDCProvider(a *org.Aggregate, writeModel *Or c.idpConfigEncryption, provider.Scopes, provider.IsIDTokenMapping, + provider.UsePKCE, provider.IDPOptions, ) if err != nil || event == nil { @@ -1512,6 +1516,11 @@ func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, writeModel *OrgLD if len(provider.UserFilters) == 0 { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-aAx9x1n", "Errors.Invalid.Argument") } + if len(provider.RootCA) > 0 { + if err := validateRootCA(provider.RootCA); err != nil { + return nil, err + } + } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -1540,6 +1549,7 @@ func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, writeModel *OrgLD provider.UserObjectClasses, provider.UserFilters, provider.Timeout, + provider.RootCA, provider.LDAPAttributes, provider.IDPOptions, ), @@ -1574,6 +1584,11 @@ func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, writeModel *Or if len(provider.UserFilters) == 0 { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-aBx901n", "Errors.Invalid.Argument") } + if len(provider.RootCA) > 0 { + if err := validateRootCA(provider.RootCA); err != nil { + return nil, err + } + } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -1600,6 +1615,7 @@ func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, writeModel *Or provider.UserObjectClasses, provider.UserFilters, provider.Timeout, + provider.RootCA, c.idpConfigEncryption, provider.LDAPAttributes, provider.IDPOptions, diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index df00096095..3baea11495 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -70,6 +70,7 @@ func (wm *OrgOAuthIDPWriteModel) NewChangedEvent( userEndpoint, idAttribute string, scopes []string, + usePKCE bool, options idp.Options, ) (*org.OAuthIDPChangedEvent, error) { @@ -83,6 +84,7 @@ func (wm *OrgOAuthIDPWriteModel) NewChangedEvent( userEndpoint, idAttribute, scopes, + usePKCE, options, ) if err != nil || len(changes) == 0 { @@ -176,7 +178,7 @@ func (wm *OrgOIDCIDPWriteModel) NewChangedEvent( clientSecretString string, secretCrypto crypto.EncryptionAlgorithm, scopes []string, - idTokenMapping bool, + idTokenMapping, usePKCE bool, options idp.Options, ) (*org.OIDCIDPChangedEvent, error) { @@ -188,6 +190,7 @@ func (wm *OrgOIDCIDPWriteModel) NewChangedEvent( secretCrypto, scopes, idTokenMapping, + usePKCE, options, ) if err != nil || len(changes) == 0 { @@ -778,6 +781,7 @@ func (wm *OrgLDAPIDPWriteModel) NewChangedEvent( userObjectClasses []string, userFilters []string, timeout time.Duration, + rootCA []byte, secretCrypto crypto.EncryptionAlgorithm, attributes idp.LDAPAttributes, options idp.Options, @@ -794,6 +798,7 @@ func (wm *OrgLDAPIDPWriteModel) NewChangedEvent( userObjectClasses, userFilters, timeout, + rootCA, secretCrypto, attributes, options, diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index 75321bb603..9959ced97d 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -210,6 +210,7 @@ func TestCommandSide_AddOrgGenericOAuthProvider(t *testing.T) { "user", "idAttribute", nil, + false, idp.Options{}, ), ), @@ -256,6 +257,7 @@ func TestCommandSide_AddOrgGenericOAuthProvider(t *testing.T) { "user", "idAttribute", []string{"user"}, + true, idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -280,6 +282,7 @@ func TestCommandSide_AddOrgGenericOAuthProvider(t *testing.T) { UserEndpoint: "user", Scopes: []string{"user"}, IDAttribute: "idAttribute", + UsePKCE: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -520,6 +523,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) { "user", "idAttribute", nil, + false, idp.Options{}, )), ), @@ -536,6 +540,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) { TokenEndpoint: "token", UserEndpoint: "user", IDAttribute: "idAttribute", + UsePKCE: false, }, }, res: res{ @@ -563,6 +568,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) { "user", "idAttribute", nil, + false, idp.Options{}, )), ), @@ -585,6 +591,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) { idp.ChangeOAuthUserEndpoint("new user"), idp.ChangeOAuthScopes([]string{"openid", "profile"}), idp.ChangeOAuthIDAttribute("newAttribute"), + idp.ChangeOAuthUsePKCE(true), idp.ChangeOAuthOptions(idp.OptionChanges{ IsCreationAllowed: &t, IsLinkingAllowed: &t, @@ -612,6 +619,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) { UserEndpoint: "new user", Scopes: []string{"openid", "profile"}, IDAttribute: "newAttribute", + UsePKCE: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -763,6 +771,7 @@ func TestCommandSide_AddOrgGenericOIDCIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, ), ), @@ -804,6 +813,7 @@ func TestCommandSide_AddOrgGenericOIDCIDP(t *testing.T) { }, []string{openid.ScopeOpenID}, true, + true, idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -826,6 +836,7 @@ func TestCommandSide_AddOrgGenericOIDCIDP(t *testing.T) { ClientSecret: "clientSecret", Scopes: []string{openid.ScopeOpenID}, IsIDTokenMapping: true, + UsePKCE: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -995,6 +1006,7 @@ func TestCommandSide_UpdateOrgGenericOIDCIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1033,6 +1045,7 @@ func TestCommandSide_UpdateOrgGenericOIDCIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1053,6 +1066,7 @@ func TestCommandSide_UpdateOrgGenericOIDCIDP(t *testing.T) { }), idp.ChangeOIDCScopes([]string{"openid", "profile"}), idp.ChangeOIDCIsIDTokenMapping(true), + idp.ChangeOIDCUsePKCE(true), idp.ChangeOIDCOptions(idp.OptionChanges{ IsCreationAllowed: &t, IsLinkingAllowed: &t, @@ -1078,6 +1092,7 @@ func TestCommandSide_UpdateOrgGenericOIDCIDP(t *testing.T) { ClientSecret: "newSecret", Scopes: []string{"openid", "profile"}, IsIDTokenMapping: true, + UsePKCE: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -1225,6 +1240,7 @@ func TestCommandSide_MigrateOrgGenericOIDCToAzureADProvider(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1284,6 +1300,7 @@ func TestCommandSide_MigrateOrgGenericOIDCToAzureADProvider(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1452,6 +1469,7 @@ func TestCommandSide_MigrateOrgOIDCToGoogleIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -1505,6 +1523,7 @@ func TestCommandSide_MigrateOrgOIDCToGoogleIDP(t *testing.T) { }, nil, false, + false, idp.Options{}, )), ), @@ -4305,6 +4324,35 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { }, }, }, + { + "invalid rootCA", + fields{ + eventstore: expectEventstore(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + StartTLS: false, + BaseDN: "baseDN", + BindDN: "dn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Second * 30, + RootCA: []byte("certificate"), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-cwqVVdBwKt", "Errors.Invalid.Argument")) + }, + }, + }, { name: "ok", fields: fields{ @@ -4328,6 +4376,7 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { []string{"object"}, []string{"filter"}, time.Second*30, + nil, idp.LDAPAttributes{}, idp.Options{}, ), @@ -4380,6 +4429,7 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { []string{"object"}, []string{"filter"}, time.Second*30, + validLDAPRootCA, idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "firstName", @@ -4421,6 +4471,7 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { UserObjectClasses: []string{"object"}, UserFilters: []string{"filter"}, Timeout: time.Second * 30, + RootCA: validLDAPRootCA, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "firstName", @@ -4655,6 +4706,31 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { }, }, }, + { + "invalid rootCA", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BaseDN: "baseDN", + BindDN: "bindDN", + UserBase: "user", + UserObjectClasses: []string{"object"}, + RootCA: []byte("certificate"), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "ORG-aBx901n", "")) + }, + }, + }, { name: "not found", fields: fields{ @@ -4706,6 +4782,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { []string{"object"}, []string{"filter"}, time.Second*30, + validLDAPRootCA, idp.LDAPAttributes{}, idp.Options{}, )), @@ -4725,6 +4802,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { UserFilters: []string{"filter"}, UserBase: "user", Timeout: time.Second * 30, + RootCA: validLDAPRootCA, }, }, res: res{ @@ -4754,6 +4832,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { []string{"object"}, []string{"filter"}, time.Second*30, + nil, idp.LDAPAttributes{}, idp.Options{}, )), @@ -4800,6 +4879,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { IsAutoCreation: &t, IsAutoUpdate: &t, }), + idp.ChangeLDAPRootCA(validLDAPRootCA), }, ) return event @@ -4823,6 +4903,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { UserObjectClasses: []string{"new object"}, UserFilters: []string{"new filter"}, Timeout: time.Second * 20, + RootCA: validLDAPRootCA, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "new id", FirstNameAttribute: "new firstName", diff --git a/internal/command/org_test.go b/internal/command/org_test.go index bf88b55a86..4ec85d61e1 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -1264,10 +1264,10 @@ func TestCommandSide_RemoveOrg(t *testing.T) { ), expectFilter( eventFromEventPusher( - project.NewSAMLConfigAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, "app1", "entity1", []byte{}, ""), + project.NewSAMLConfigAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, "app1", "entity1", []byte{}, "", domain.LoginVersionUnspecified, ""), ), eventFromEventPusher( - project.NewSAMLConfigAddedEvent(context.Background(), &project.NewAggregate("project2", "org1").Aggregate, "app2", "entity2", []byte{}, ""), + project.NewSAMLConfigAddedEvent(context.Background(), &project.NewAggregate("project2", "org1").Aggregate, "app2", "entity2", []byte{}, "", domain.LoginVersionUnspecified, ""), ), ), expectPush( diff --git a/internal/command/project_application_oidc_model.go b/internal/command/project_application_oidc_model.go index 3fc07c79a9..603ebdcda2 100644 --- a/internal/command/project_application_oidc_model.go +++ b/internal/command/project_application_oidc_model.go @@ -325,10 +325,10 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent( changes = append(changes, project.ChangeBackChannelLogoutURI(backChannelLogoutURI)) } if wm.LoginVersion != loginVersion { - changes = append(changes, project.ChangeLoginVersion(loginVersion)) + changes = append(changes, project.ChangeOIDCLoginVersion(loginVersion)) } if wm.LoginBaseURI != loginBaseURI { - changes = append(changes, project.ChangeLoginBaseURI(loginBaseURI)) + changes = append(changes, project.ChangeOIDCLoginBaseURI(loginBaseURI)) } if len(changes) == 0 { diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go index 8b663afa57..4b9f5bf94f 100644 --- a/internal/command/project_application_oidc_test.go +++ b/internal/command/project_application_oidc_test.go @@ -1297,8 +1297,8 @@ func newOIDCAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner project.ChangeIDTokenRoleAssertion(false), project.ChangeIDTokenUserinfoAssertion(false), project.ChangeClockSkew(time.Second * 2), - project.ChangeLoginVersion(domain.LoginVersion2), - project.ChangeLoginBaseURI("https://login.test.ch"), + project.ChangeOIDCLoginVersion(domain.LoginVersion2), + project.ChangeOIDCLoginBaseURI("https://login.test.ch"), } event, _ := project.NewOIDCConfigChangedEvent(ctx, &project.NewAggregate(projectID, resourceOwner).Aggregate, diff --git a/internal/command/project_application_saml.go b/internal/command/project_application_saml.go index 76297ad93f..b14bed0758 100644 --- a/internal/command/project_application_saml.go +++ b/internal/command/project_application_saml.go @@ -79,6 +79,8 @@ func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstor string(entity.EntityID), samlApp.Metadata, samlApp.MetadataURL, + samlApp.LoginVersion, + samlApp.LoginBaseURI, ), }, nil } @@ -119,7 +121,10 @@ func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SA samlApp.AppID, string(entity.EntityID), samlApp.Metadata, - samlApp.MetadataURL) + samlApp.MetadataURL, + samlApp.LoginVersion, + samlApp.LoginBaseURI, + ) if err != nil { return nil, err } diff --git a/internal/command/project_application_saml_model.go b/internal/command/project_application_saml_model.go index 2652acc617..f219039b58 100644 --- a/internal/command/project_application_saml_model.go +++ b/internal/command/project_application_saml_model.go @@ -12,11 +12,13 @@ import ( type SAMLApplicationWriteModel struct { eventstore.WriteModel - AppID string - AppName string - EntityID string - Metadata []byte - MetadataURL string + AppID string + AppName string + EntityID string + Metadata []byte + MetadataURL string + LoginVersion domain.LoginVersion + LoginBaseURI string State domain.AppState saml bool @@ -121,6 +123,8 @@ func (wm *SAMLApplicationWriteModel) appendAddSAMLEvent(e *project.SAMLConfigAdd wm.Metadata = e.Metadata wm.MetadataURL = e.MetadataURL wm.EntityID = e.EntityID + wm.LoginVersion = e.LoginVersion + wm.LoginBaseURI = e.LoginBaseURI } func (wm *SAMLApplicationWriteModel) appendChangeSAMLEvent(e *project.SAMLConfigChangedEvent) { @@ -134,6 +138,12 @@ func (wm *SAMLApplicationWriteModel) appendChangeSAMLEvent(e *project.SAMLConfig if e.EntityID != "" { wm.EntityID = e.EntityID } + if e.LoginVersion != nil { + wm.LoginVersion = *e.LoginVersion + } + if e.LoginBaseURI != nil { + wm.LoginBaseURI = *e.LoginBaseURI + } } func (wm *SAMLApplicationWriteModel) Query() *eventstore.SearchQueryBuilder { @@ -161,6 +171,8 @@ func (wm *SAMLApplicationWriteModel) NewChangedEvent( entityID string, metadata []byte, metadataURL string, + loginVersion domain.LoginVersion, + loginBaseURI string, ) (*project.SAMLConfigChangedEvent, bool, error) { changes := make([]project.SAMLConfigChanges, 0) var err error @@ -173,6 +185,12 @@ func (wm *SAMLApplicationWriteModel) NewChangedEvent( if wm.EntityID != entityID { changes = append(changes, project.ChangeEntityID(entityID)) } + if wm.LoginVersion != loginVersion { + changes = append(changes, project.ChangeSAMLLoginVersion(loginVersion)) + } + if wm.LoginBaseURI != loginBaseURI { + changes = append(changes, project.ChangeSAMLLoginBaseURI(loginBaseURI)) + } if len(changes) == 0 { return nil, false, nil diff --git a/internal/command/project_application_saml_test.go b/internal/command/project_application_saml_test.go index ff774e9f49..c6f6f7cf21 100644 --- a/internal/command/project_application_saml_test.go +++ b/internal/command/project_application_saml_test.go @@ -3,7 +3,7 @@ package command import ( "bytes" "context" - "io/ioutil" + "io" "net/http" "testing" @@ -50,7 +50,7 @@ var testMetadataChangedEntityID = []byte(` func TestCommandSide_AddSAMLApplication(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator httpClient *http.Client } @@ -72,9 +72,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { { name: "no aggregate id, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), @@ -88,8 +86,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -111,8 +108,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { { name: "invalid app, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -141,8 +137,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { { name: "create saml app, metadata not parsable", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -174,8 +169,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { { name: "create saml app, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -196,6 +190,8 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { "https://test.com/saml/metadata", testMetadata, "", + domain.LoginVersionUnspecified, + "", ), ), ), @@ -229,11 +225,73 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { }, }, }, + { + name: "create saml app, loginversion, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingUnspecified), + ), + ), + expectPush( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "https://test.com/saml/metadata", + testMetadata, + "", + domain.LoginVersion2, + "https://test.com/login", + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1"), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + samlApp: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppName: "app", + EntityID: "https://test.com/saml/metadata", + Metadata: testMetadata, + MetadataURL: "", + LoginVersion: domain.LoginVersion2, + LoginBaseURI: "https://test.com/login", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + EntityID: "https://test.com/saml/metadata", + Metadata: testMetadata, + MetadataURL: "", + State: domain.AppStateActive, + LoginVersion: domain.LoginVersion2, + LoginBaseURI: "https://test.com/login", + }, + }, + }, { name: "create saml app metadataURL, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -254,6 +312,8 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { "https://test.com/saml/metadata", testMetadata, "http://localhost:8080/saml/metadata", + domain.LoginVersionUnspecified, + "", ), ), ), @@ -291,8 +351,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { { name: "create saml app metadataURL, http error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -327,7 +386,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, httpClient: tt.fields.httpClient, } @@ -348,7 +407,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { func TestCommandSide_ChangeSAMLApplication(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore httpClient *http.Client } type args struct { @@ -369,9 +428,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { { name: "invalid app, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -390,9 +447,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { { name: "missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -412,9 +467,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { { name: "missing aggregateid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -434,8 +487,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -457,8 +509,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { { name: "no changes, precondition error, metadataURL", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewApplicationAddedEvent(context.Background(), @@ -474,6 +525,8 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { "https://test.com/saml/metadata", testMetadata, "http://localhost:8080/saml/metadata", + domain.LoginVersionUnspecified, + "", ), ), ), @@ -502,8 +555,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { { name: "no changes, precondition error, metadata", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewApplicationAddedEvent(context.Background(), @@ -519,6 +571,8 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { "https://test.com/saml/metadata", testMetadata, "", + domain.LoginVersionUnspecified, + "", ), ), ), @@ -547,8 +601,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { { name: "change saml app, ok, metadataURL", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewApplicationAddedEvent(context.Background(), @@ -564,6 +617,8 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { "https://test.com/saml/metadata", testMetadata, "http://localhost:8080/saml/metadata", + domain.LoginVersionUnspecified, + "", ), ), ), @@ -613,8 +668,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { { name: "change saml app, ok, metadata", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewApplicationAddedEvent(context.Background(), @@ -630,6 +684,8 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { "https://test.com/saml/metadata", testMetadata, "", + domain.LoginVersionUnspecified, + "", ), ), ), @@ -675,13 +731,85 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { State: domain.AppStateActive, }, }, + }, { + name: "change saml app, ok, loginversion", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "https://test.com/saml/metadata", + testMetadata, + "", + domain.LoginVersionUnspecified, + "", + ), + ), + ), + expectPush( + newSAMLAppChangedEventLoginVersion(context.Background(), + "app1", + "project1", + "org1", + "https://test.com/saml/metadata", + "https://test2.com/saml/metadata", + testMetadataChangedEntityID, + domain.LoginVersion2, + "https://test.com/login", + ), + ), + ), + httpClient: nil, + }, + args: args{ + ctx: context.Background(), + samlApp: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + EntityID: "https://test2.com/saml/metadata", + Metadata: testMetadataChangedEntityID, + MetadataURL: "", + LoginVersion: domain.LoginVersion2, + LoginBaseURI: "https://test.com/login", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + EntityID: "https://test2.com/saml/metadata", + Metadata: testMetadataChangedEntityID, + MetadataURL: "", + State: domain.AppStateActive, + LoginVersion: domain.LoginVersion2, + LoginBaseURI: "https://test.com/login", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), httpClient: tt.fields.httpClient, } got, err := r.ChangeSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner) @@ -726,6 +854,22 @@ func newSAMLAppChangedEventMetadataURL(ctx context.Context, appID, projectID, re return event } +func newSAMLAppChangedEventLoginVersion(ctx context.Context, appID, projectID, resourceOwner, oldEntityID, entityID string, metadata []byte, loginVersion domain.LoginVersion, loginURI string) *project.SAMLConfigChangedEvent { + changes := []project.SAMLConfigChanges{ + project.ChangeEntityID(entityID), + project.ChangeMetadata(metadata), + project.ChangeSAMLLoginVersion(loginVersion), + project.ChangeSAMLLoginBaseURI(loginURI), + } + event, _ := project.NewSAMLConfigChangedEvent(ctx, + &project.NewAggregate(projectID, resourceOwner).Aggregate, + appID, + oldEntityID, + changes, + ) + return event +} + type roundTripperFunc func(*http.Request) *http.Response // RoundTrip implements the http.RoundTripper interface. @@ -738,7 +882,7 @@ func newTestClient(httpStatus int, metadata []byte) *http.Client { fn := roundTripperFunc(func(req *http.Request) *http.Response { return &http.Response{ StatusCode: httpStatus, - Body: ioutil.NopCloser(bytes.NewBuffer(metadata)), + Body: io.NopCloser(bytes.NewBuffer(metadata)), Header: make(http.Header), //must be non-nil value } }) diff --git a/internal/command/project_application_test.go b/internal/command/project_application_test.go index ae2c6c39b0..050a41d29f 100644 --- a/internal/command/project_application_test.go +++ b/internal/command/project_application_test.go @@ -596,6 +596,8 @@ func TestCommandSide_RemoveApplication(t *testing.T) { "https://test.com/saml/metadata", []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), "", + domain.LoginVersionUnspecified, + "", )), ), expectPush( diff --git a/internal/command/project_converter.go b/internal/command/project_converter.go index 59343aa762..01b5a4e63d 100644 --- a/internal/command/project_converter.go +++ b/internal/command/project_converter.go @@ -55,13 +55,15 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.SAMLApp { return &domain.SAMLApp{ - ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel), - AppID: writeModel.AppID, - AppName: writeModel.AppName, - State: writeModel.State, - Metadata: writeModel.Metadata, - MetadataURL: writeModel.MetadataURL, - EntityID: writeModel.EntityID, + ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel), + AppID: writeModel.AppID, + AppName: writeModel.AppName, + State: writeModel.State, + Metadata: writeModel.Metadata, + MetadataURL: writeModel.MetadataURL, + EntityID: writeModel.EntityID, + LoginVersion: writeModel.LoginVersion, + LoginBaseURI: writeModel.LoginBaseURI, } } diff --git a/internal/command/project_test.go b/internal/command/project_test.go index 645371e2fc..842e1aa640 100644 --- a/internal/command/project_test.go +++ b/internal/command/project_test.go @@ -988,6 +988,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { "https://test.com/saml/metadata", []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), "http://localhost:8080/saml/metadata", + domain.LoginVersionUnspecified, + "", ), ), ), @@ -1039,6 +1041,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { "https://test1.com/saml/metadata", []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), "", + domain.LoginVersionUnspecified, + "", ), ), eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), @@ -1053,6 +1057,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { "https://test2.com/saml/metadata", []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), "", + domain.LoginVersionUnspecified, + "", ), ), eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), @@ -1067,6 +1073,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { "https://test3.com/saml/metadata", []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), "", + domain.LoginVersionUnspecified, + "", ), ), ), diff --git a/internal/command/saml_request.go b/internal/command/saml_request.go index 2dfa8756c7..40e0643f0c 100644 --- a/internal/command/saml_request.go +++ b/internal/command/saml_request.go @@ -15,13 +15,14 @@ type SAMLRequest struct { ID string LoginClient string - ApplicationID string - ACSURL string - RelayState string - RequestID string - Binding string - Issuer string - Destination string + ApplicationID string + ACSURL string + RelayState string + RequestID string + Binding string + Issuer string + Destination string + ResponseIssuer string } type CurrentSAMLRequest struct { @@ -56,6 +57,7 @@ func (c *Commands) AddSAMLRequest(ctx context.Context, samlRequest *SAMLRequest) samlRequest.Binding, samlRequest.Issuer, samlRequest.Destination, + samlRequest.ResponseIssuer, )) if err != nil { return nil, err @@ -75,7 +77,9 @@ func (c *Commands) LinkSessionToSAMLRequest(ctx context.Context, id, sessionID, return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-ttPKNdAIFT", "Errors.SAMLRequest.AlreadyHandled") } if checkLoginClient && authz.GetCtxData(ctx).UserID != writeModel.LoginClient { - return nil, nil, zerrors.ThrowPermissionDenied(nil, "COMMAND-KCd48Rxt7x", "Errors.SAMLRequest.WrongLoginClient") + if err := c.checkPermission(ctx, domain.PermissionSessionLink, writeModel.ResourceOwner, ""); err != nil { + return nil, nil, err + } } sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) @@ -129,15 +133,16 @@ func (c *Commands) FailSAMLRequest(ctx context.Context, id string, reason domain func samlRequestWriteModelToCurrentSAMLRequest(writeModel *SAMLRequestWriteModel) (_ *CurrentSAMLRequest) { return &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: writeModel.AggregateID, - LoginClient: writeModel.LoginClient, - ApplicationID: writeModel.ApplicationID, - ACSURL: writeModel.ACSURL, - RelayState: writeModel.RelayState, - RequestID: writeModel.RequestID, - Binding: writeModel.Binding, - Issuer: writeModel.Issuer, - Destination: writeModel.Destination, + ID: writeModel.AggregateID, + LoginClient: writeModel.LoginClient, + ApplicationID: writeModel.ApplicationID, + ACSURL: writeModel.ACSURL, + RelayState: writeModel.RelayState, + RequestID: writeModel.RequestID, + Binding: writeModel.Binding, + Issuer: writeModel.Issuer, + Destination: writeModel.Destination, + ResponseIssuer: writeModel.ResponseIssuer, }, SessionID: writeModel.SessionID, UserID: writeModel.UserID, diff --git a/internal/command/saml_request_model.go b/internal/command/saml_request_model.go index 7ba640cbe8..afd5b052c7 100644 --- a/internal/command/saml_request_model.go +++ b/internal/command/saml_request_model.go @@ -15,14 +15,15 @@ type SAMLRequestWriteModel struct { eventstore.WriteModel aggregate *eventstore.Aggregate - LoginClient string - ApplicationID string - ACSURL string - RelayState string - RequestID string - Binding string - Issuer string - Destination string + LoginClient string + ApplicationID string + ACSURL string + RelayState string + RequestID string + Binding string + Issuer string + Destination string + ResponseIssuer string SessionID string UserID string @@ -52,6 +53,7 @@ func (m *SAMLRequestWriteModel) Reduce() error { m.Binding = e.Binding m.Issuer = e.Issuer m.Destination = e.Destination + m.ResponseIssuer = e.ResponseIssuer m.SAMLRequestState = domain.SAMLRequestStateAdded case *samlrequest.SessionLinkedEvent: m.SessionID = e.SessionID diff --git a/internal/command/saml_request_test.go b/internal/command/saml_request_test.go index ed7363e151..c11c87ec48 100644 --- a/internal/command/saml_request_test.go +++ b/internal/command/saml_request_test.go @@ -54,6 +54,7 @@ func TestCommands_AddSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -82,6 +83,7 @@ func TestCommands_AddSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -90,27 +92,29 @@ func TestCommands_AddSAMLRequest(t *testing.T) { args{ ctx: mockCtx, request: &SAMLRequest{ - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, nil, @@ -132,8 +136,9 @@ func TestCommands_AddSAMLRequest(t *testing.T) { func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient") type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) + eventstore func(t *testing.T) *eventstore.Eventstore + tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -186,6 +191,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -207,7 +213,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { }, }, { - "wrong login client", + "wrong login client / not permitted", fields{ eventstore: expectEventstore( expectFilter( @@ -221,11 +227,13 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), ), - tokenVerifier: newMockTokenVerifierValid(), + tokenVerifier: newMockTokenVerifierValid(), + checkPermission: newMockPermissionCheckNotAllowed(), }, args{ ctx: authz.NewMockContext("instanceID", "orgID", "wrongLoginClient"), @@ -235,7 +243,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { checkLoginClient: true, }, res{ - wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-KCd48Rxt7x", "Errors.SAMLRequest.WrongLoginClient"), + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, }, { @@ -253,6 +261,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -284,6 +293,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -338,6 +348,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -381,6 +392,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -429,15 +441,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -460,6 +473,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -509,15 +523,98 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "loginClient", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", + }, + SessionID: "sessionID", + UserID: "userID", + AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + }, + }, + }, { + "linked with permission", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate, + "loginClient", + "application", + "acs", + "relaystate", + "request", + "binding", + "issuer", + "destination", + "responseissuer", + ), + ), + ), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + eventFromEventPusher( + session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + expectPush( + samlrequest.NewSessionLinkedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate, + "sessionID", + "userID", + testNow, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"), + id: "V2_id", + sessionID: "sessionID", + sessionToken: "token", + checkLoginClient: true, + }, + res{ + details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, + authReq: &CurrentSAMLRequest{ + SAMLRequest: &SAMLRequest{ + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -540,6 +637,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -590,15 +688,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "loginClient", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -621,6 +720,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -669,6 +769,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), sessionTokenVerifier: tt.fields.tokenVerifier, + checkPermission: tt.fields.checkPermission, } details, got, err := c.LinkSessionToSAMLRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient, tt.args.checkPermission) require.ErrorIs(t, err, tt.res.wantErr) @@ -734,6 +835,7 @@ func TestCommands_FailSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate, @@ -767,6 +869,7 @@ func TestCommands_FailSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -787,15 +890,16 @@ func TestCommands_FailSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, samlReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, }, diff --git a/internal/command/saml_session_test.go b/internal/command/saml_session_test.go index 12cc0683c5..4781381cc4 100644 --- a/internal/command/saml_session_test.go +++ b/internal/command/saml_session_test.go @@ -99,6 +99,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -129,6 +130,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -167,6 +169,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -248,6 +251,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 5fba985148..60027d3a05 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -837,6 +837,7 @@ func TestCommands_updateSession(t *testing.T) { nil, nil, "idpID", + nil, ), ), eventFromEventPusher( diff --git a/internal/command/statics.go b/internal/command/statics.go index b68bebc221..876359baad 100644 --- a/internal/command/statics.go +++ b/internal/command/statics.go @@ -6,7 +6,7 @@ import ( "io" "strings" - "github.com/superseriousbusiness/exifremove/pkg/exifremove" + "github.com/zitadel/exifremove/pkg/exifremove" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/static" diff --git a/internal/command/system_features.go b/internal/command/system_features.go index dc886de318..b317ea93bb 100644 --- a/internal/command/system_features.go +++ b/internal/command/system_features.go @@ -15,7 +15,6 @@ type SystemFeatures struct { LegacyIntrospection *bool TokenExchange *bool UserSchema *bool - Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType OIDCSingleV1SessionTermination *bool DisableUserTokenEvent *bool @@ -30,7 +29,6 @@ func (m *SystemFeatures) isEmpty() bool { m.LegacyIntrospection == nil && m.UserSchema == nil && m.TokenExchange == nil && - m.Actions == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && m.OIDCSingleV1SessionTermination == nil && diff --git a/internal/command/system_features_model.go b/internal/command/system_features_model.go index 15fc3e0bf0..28e56f8bd4 100644 --- a/internal/command/system_features_model.go +++ b/internal/command/system_features_model.go @@ -64,7 +64,6 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.SystemLegacyIntrospectionEventType, feature_v2.SystemUserSchemaEventType, feature_v2.SystemTokenExchangeEventType, - feature_v2.SystemActionsEventType, feature_v2.SystemImprovedPerformanceEventType, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, feature_v2.SystemDisableUserTokenEvent, @@ -98,9 +97,6 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) { case feature.KeyTokenExchange: v := value.(bool) features.TokenExchange = &v - case feature.KeyActions: - v := value.(bool) - features.Actions = &v case feature.KeyImprovedPerformance: features.ImprovedPerformance = value.([]feature.ImprovedPerformanceType) case feature.KeyOIDCSingleV1SessionTermination: @@ -128,7 +124,6 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.SystemLegacyIntrospectionEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.SystemUserSchemaEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.SystemTokenExchangeEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.SystemActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.SystemImprovedPerformanceEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.SystemOIDCSingleV1SessionTerminationEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.SystemDisableUserTokenEvent) diff --git a/internal/command/system_features_test.go b/internal/command/system_features_test.go index 9c5f4cc2a9..b1b5207b8c 100644 --- a/internal/command/system_features_test.go +++ b/internal/command/system_features_test.go @@ -117,24 +117,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { ResourceOwner: "SYSTEM", }, }, - { - name: "set Actions", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - ), - ), - ), - args: args{context.Background(), &SystemFeatures{ - Actions: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "SYSTEM", - }, - }, { name: "push error", eventstore: expectEventstore( @@ -172,10 +154,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - ), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, true, @@ -187,7 +165,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ @@ -233,10 +210,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, true, ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - ), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, false, @@ -248,7 +221,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), - Actions: gu.Ptr(false), OIDCSingleV1SessionTermination: gu.Ptr(false), }}, want: &domain.ObjectDetails{ diff --git a/internal/command/user_schema_model.go b/internal/command/user_schema_model.go index 08352e6d57..f58a689d3a 100644 --- a/internal/command/user_schema_model.go +++ b/internal/command/user_schema_model.go @@ -4,8 +4,7 @@ import ( "bytes" "context" "encoding/json" - - "golang.org/x/exp/slices" + "slices" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -80,6 +79,7 @@ func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder { return query.Builder() } + func (wm *UserSchemaWriteModel) NewUpdatedEvent( ctx context.Context, agg *eventstore.Aggregate, diff --git a/internal/command/web_key.go b/internal/command/web_key.go index e8481541c3..b46d5fc1fc 100644 --- a/internal/command/web_key.go +++ b/internal/command/web_key.go @@ -2,6 +2,7 @@ package command import ( "context" + "time" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" @@ -156,27 +157,28 @@ func (c *Commands) getAllWebKeys(ctx context.Context) (_ map[string]*WebKeyWrite return models.keys, models.activeID, nil } -func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) { +func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ time.Time, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() model := NewWebKeyWriteModel(keyID, authz.GetInstance(ctx).InstanceID()) if err = c.eventstore.FilterToQueryReducer(ctx, model); err != nil { - return nil, err + return time.Time{}, err } - if model.State == domain.WebKeyStateUnspecified { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound") + if model.State == domain.WebKeyStateUnspecified || + model.State == domain.WebKeyStateRemoved { + return model.WriteModel.ChangeDate, nil } if model.State == domain.WebKeyStateActive { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete") + return time.Time{}, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete") } err = c.pushAppendAndReduce(ctx, model, webkey.NewRemovedEvent(ctx, webkey.AggregateFromWriteModel(ctx, &model.WriteModel), )) if err != nil { - return nil, err + return time.Time{}, err } - return writeModelToObjectDetails(&model.WriteModel), nil + return model.WriteModel.ChangeDate, nil } func (c *Commands) prepareGenerateInitialWebKeys(instanceID string, conf crypto.WebKeyConfig) preparation.Validation { diff --git a/internal/command/web_key_test.go b/internal/command/web_key_test.go index 63463de1df..13587d0f68 100644 --- a/internal/command/web_key_test.go +++ b/internal/command/web_key_test.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "io" "testing" + "time" "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" @@ -610,7 +611,7 @@ func TestCommands_DeleteWebKey(t *testing.T) { name string fields fields args args - want *domain.ObjectDetails + want time.Time wantErr error }{ { @@ -624,14 +625,73 @@ func TestCommands_DeleteWebKey(t *testing.T) { wantErr: io.ErrClosedPipe, }, { - name: "not found error", + name: "not found", fields: fields{ eventstore: expectEventstore( expectFilter(), ), }, - args: args{"key1"}, - wantErr: zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound"), + args: args{"key1"}, + want: time.Time{}, + }, + { + name: "previously deleted", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key2", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + )), + eventFromEventPusher(webkey.NewDeactivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + eventFromEventPusher(webkey.NewRemovedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + ), + }, + args: args{"key1"}, + want: time.Time{}, }, { name: "key active error", @@ -722,10 +782,7 @@ func TestCommands_DeleteWebKey(t *testing.T) { ), }, args: args{"key1"}, - want: &domain.ObjectDetails{ - ResourceOwner: "instance1", - ID: "key1", - }, + want: time.Time{}, }, } for _, tt := range tests { diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go index 6ff0f4ea10..e14c2dfaaf 100644 --- a/internal/crypto/passwap.go +++ b/internal/crypto/passwap.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/passwap/bcrypt" "github.com/zitadel/passwap/md5" "github.com/zitadel/passwap/md5plain" + "github.com/zitadel/passwap/md5salted" "github.com/zitadel/passwap/pbkdf2" "github.com/zitadel/passwap/scrypt" "github.com/zitadel/passwap/verifier" @@ -43,14 +44,15 @@ func (h *Hasher) EncodingSupported(encodedHash string) bool { type HashName string const ( - HashNameArgon2 HashName = "argon2" // used for the common argon2 verifier - HashNameArgon2i HashName = "argon2i" // hash only - HashNameArgon2id HashName = "argon2id" // hash only - HashNameBcrypt HashName = "bcrypt" // hash and verify - HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated - HashNameMd5Plain HashName = "md5plain" // verify only, as hashing with md5 is insecure and deprecated - HashNameScrypt HashName = "scrypt" // hash and verify - HashNamePBKDF2 HashName = "pbkdf2" // hash and verify + HashNameArgon2 HashName = "argon2" // used for the common argon2 verifier + HashNameArgon2i HashName = "argon2i" // hash only + HashNameArgon2id HashName = "argon2id" // hash only + HashNameBcrypt HashName = "bcrypt" // hash and verify + HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated + HashNameMd5Plain HashName = "md5plain" // verify only, as hashing with md5 is insecure and deprecated + HashNameMd5Salted HashName = "md5salted" // verify only, as hashing with md5 is insecure and deprecated + HashNameScrypt HashName = "scrypt" // hash and verify + HashNamePBKDF2 HashName = "pbkdf2" // hash and verify ) type HashMode string @@ -119,6 +121,10 @@ var knowVerifiers = map[HashName]prefixVerifier{ prefixes: []string{pbkdf2.Prefix}, verifier: pbkdf2.Verifier, }, + HashNameMd5Salted: { + prefixes: []string{md5salted.Prefix}, + verifier: md5salted.Verifier, + }, } func (c *HashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) { diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go index cbc7202501..b872b0e298 100644 --- a/internal/crypto/passwap_test.go +++ b/internal/crypto/passwap_test.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/passwap/argon2" "github.com/zitadel/passwap/bcrypt" "github.com/zitadel/passwap/md5" + "github.com/zitadel/passwap/md5salted" "github.com/zitadel/passwap/pbkdf2" "github.com/zitadel/passwap/scrypt" ) @@ -76,6 +77,7 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { HashNameArgon2, HashNameBcrypt, HashNameMd5, + HashNameMd5Salted, HashNameScrypt, "foobar", }, @@ -122,6 +124,24 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { }, wantErr: true, }, + { + name: "invalid md5plain", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameMd5Plain, + }, + }, + wantErr: true, + }, + { + name: "invalid md5salted", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameMd5Salted, + }, + }, + wantErr: true, + }, { name: "invalid argon2", fields: fields{ @@ -160,9 +180,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "threads": 4, }, }, - Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt}, + Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux}, + wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux, md5salted.Prefix}, }, { name: "argon2id, error", @@ -188,9 +208,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "threads": 4, }, }, - Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt}, + Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux}, + wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux, md5salted.Prefix}, }, { name: "bcrypt, error", @@ -213,9 +233,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "cost": 3, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameMd5, HashNameScrypt}, + Verifiers: []HashName{HashNameArgon2, HashNameMd5, HashNameScrypt, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{bcrypt.Prefix, argon2.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux}, + wantPrefixes: []string{bcrypt.Prefix, argon2.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux, md5salted.Prefix}, }, { name: "scrypt, error", @@ -238,9 +258,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "cost": 3, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, parse error", @@ -277,9 +297,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA1, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, sha224", @@ -291,9 +311,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA224, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, sha256", @@ -305,9 +325,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA256, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, sha384", @@ -319,9 +339,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA384, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, { name: "pbkdf2, sha512", @@ -333,9 +353,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { "Hash": HashModeSHA512, }, }, - Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5}, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted}, }, - wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, } for _, tt := range tests { diff --git a/internal/database/cockroach/crdb.go b/internal/database/cockroach/crdb.go index a5b3208a86..f89792c0c8 100644 --- a/internal/database/cockroach/crdb.go +++ b/internal/database/cockroach/crdb.go @@ -18,7 +18,7 @@ import ( func init() { config := new(Config) - dialect.Register(config, config, true) + dialect.Register(config, config, false) } const ( @@ -52,7 +52,7 @@ func (c *Config) MatchName(name string) bool { return false } -func (_ *Config) Decode(configs []interface{}) (dialect.Connector, error) { +func (_ *Config) Decode(configs []any) (dialect.Connector, error) { connector := new(Config) decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ DecodeHook: mapstructure.StringToTimeDurationHookFunc(), @@ -73,12 +73,6 @@ func (_ *Config) Decode(configs []interface{}) (dialect.Connector, error) { } func (c *Config) Connect(useAdmin bool) (*sql.DB, *pgxpool.Pool, error) { - dialect.RegisterAfterConnect(func(ctx context.Context, c *pgx.Conn) error { - // CockroachDB by default does not allow multiple modifications of the same table using ON CONFLICT - // This is needed to fill the fields table of the eventstore during eventstore.Push. - _, err := c.Exec(ctx, "SET enable_multiple_modifications_of_table = on") - return err - }) connConfig := dialect.NewConnectionConfig(c.MaxOpenConns, c.MaxIdleConns) config, err := pgxpool.ParseConfig(c.String(useAdmin)) @@ -149,12 +143,8 @@ func (c *Config) Password() string { return c.User.Password } -func (c *Config) Type() string { - return "cockroach" -} - -func (c *Config) Timetravel(d time.Duration) string { - return "" +func (c *Config) Type() dialect.DatabaseType { + return dialect.DatabaseTypeCockroach } type User struct { diff --git a/internal/database/database.go b/internal/database/database.go index e254edadc1..ddc26a7961 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -149,33 +149,40 @@ func Connect(config Config, useAdmin bool) (*DB, error) { }, nil } -func DecodeHook(from, to reflect.Value) (_ interface{}, err error) { - if to.Type() != reflect.TypeOf(Config{}) { - return from.Interface(), nil - } - - config := new(Config) - if err = mapstructure.Decode(from.Interface(), config); err != nil { - return nil, err - } - - configuredDialect := dialect.SelectByConfig(config.Dialects) - configs := make([]interface{}, 0, len(config.Dialects)-1) - - for name, dialectConfig := range config.Dialects { - if !configuredDialect.Matcher.MatchName(name) { - continue +func DecodeHook(allowCockroach bool) func(from, to reflect.Value) (_ interface{}, err error) { + return func(from, to reflect.Value) (_ interface{}, err error) { + if to.Type() != reflect.TypeOf(Config{}) { + return from.Interface(), nil } - configs = append(configs, dialectConfig) - } + config := new(Config) + if err = mapstructure.Decode(from.Interface(), config); err != nil { + return nil, err + } - config.connector, err = configuredDialect.Matcher.Decode(configs) - if err != nil { - return nil, err - } + configuredDialect := dialect.SelectByConfig(config.Dialects) + configs := make([]any, 0, len(config.Dialects)) - return config, nil + for name, dialectConfig := range config.Dialects { + if !configuredDialect.Matcher.MatchName(name) { + continue + } + + configs = append(configs, dialectConfig) + } + + if !allowCockroach && configuredDialect.Matcher.Type() == dialect.DatabaseTypeCockroach { + logging.Info("Cockroach support was removed with Zitadel v3, please refer to https://zitadel.com/docs/self-hosting/manage/cli/mirror to migrate your data to postgres") + return nil, zerrors.ThrowPreconditionFailed(nil, "DATAB-0pIWD", "Cockroach support was removed with Zitadel v3") + } + + config.connector, err = configuredDialect.Matcher.Decode(configs) + if err != nil { + return nil, err + } + + return config, nil + } } func (c Config) DatabaseName() string { @@ -190,7 +197,7 @@ func (c Config) Password() string { return c.connector.Password() } -func (c Config) Type() string { +func (c Config) Type() dialect.DatabaseType { return c.connector.Type() } diff --git a/internal/database/dialect/config.go b/internal/database/dialect/config.go index 71fb477ea1..16068daadd 100644 --- a/internal/database/dialect/config.go +++ b/internal/database/dialect/config.go @@ -3,7 +3,6 @@ package dialect import ( "database/sql" "sync" - "time" "github.com/jackc/pgx/v5/pgxpool" ) @@ -22,9 +21,17 @@ var ( type Matcher interface { MatchName(string) bool - Decode([]interface{}) (Connector, error) + Decode([]any) (Connector, error) + Type() DatabaseType } +type DatabaseType uint8 + +const ( + DatabaseTypePostgres DatabaseType = iota + DatabaseTypeCockroach +) + const ( DefaultAppName = "zitadel" ) @@ -38,8 +45,7 @@ type Connector interface { type Database interface { DatabaseName() string Username() string - Type() string - Timetravel(time.Duration) string + Type() DatabaseType } func Register(matcher Matcher, config Connector, isDefault bool) { diff --git a/internal/database/postgres/embedded.go b/internal/database/postgres/embedded.go new file mode 100644 index 0000000000..57aec756f0 --- /dev/null +++ b/internal/database/postgres/embedded.go @@ -0,0 +1,38 @@ +package postgres + +import ( + "net" + "os" + + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + "github.com/zitadel/logging" +) + +func StartEmbedded() (embeddedpostgres.Config, func()) { + path, err := os.MkdirTemp("", "zitadel-embedded-postgres-*") + logging.OnError(err).Fatal("unable to create temp dir") + + port, close := getPort() + + config := embeddedpostgres.DefaultConfig().Version(embeddedpostgres.V16).Port(uint32(port)).RuntimePath(path) + embedded := embeddedpostgres.NewDatabase(config) + + close() + err = embedded.Start() + logging.OnError(err).Fatal("unable to start db") + + return config, func() { + logging.OnError(embedded.Stop()).Error("unable to stop db") + } +} + +// getPort returns a free port and locks it until close is called +func getPort() (port uint16, close func()) { + l, err := net.Listen("tcp", ":0") + logging.OnError(err).Fatal("unable to get port") + port = uint16(l.Addr().(*net.TCPAddr).Port) + logging.WithFields("port", port).Info("Port is available") + return port, func() { + logging.OnError(l.Close()).Error("unable to close port listener") + } +} diff --git a/internal/database/postgres/pg.go b/internal/database/postgres/pg.go index c847cc0a58..2f8bb29e17 100644 --- a/internal/database/postgres/pg.go +++ b/internal/database/postgres/pg.go @@ -18,7 +18,7 @@ import ( func init() { config := new(Config) - dialect.Register(config, config, false) + dialect.Register(config, config, true) } const ( @@ -29,16 +29,15 @@ const ( ) type Config struct { - Host string - Port int32 - Database string - EventPushConnRatio float64 - MaxOpenConns uint32 - MaxIdleConns uint32 - MaxConnLifetime time.Duration - MaxConnIdleTime time.Duration - User User - Admin AdminUser + Host string + Port int32 + Database string + MaxOpenConns uint32 + MaxIdleConns uint32 + MaxConnLifetime time.Duration + MaxConnIdleTime time.Duration + User User + Admin AdminUser // Additional options to be appended as options= // The value will be taken as is. Multiple options are space separated. Options string @@ -148,12 +147,8 @@ func (c *Config) Password() string { return c.User.Password } -func (c *Config) Type() string { - return "postgres" -} - -func (c *Config) Timetravel(time.Duration) string { - return "" +func (c *Config) Type() dialect.DatabaseType { + return dialect.DatabaseTypePostgres } type User struct { diff --git a/internal/database/type.go b/internal/database/type.go index 6a781288a9..bd07e0bfde 100644 --- a/internal/database/type.go +++ b/internal/database/type.go @@ -225,3 +225,37 @@ func (d *NullDuration) Scan(src any) error { d.Duration, d.Valid = time.Duration(*duration), true return nil } + +// JSONArray allows sending and receiving JSON arrays to and from the database. +// It implements the [database/sql.Scanner] and [database/sql/driver.Valuer] interfaces. +// Values are marshaled and unmarshaled using the [encoding/json] package. +type JSONArray[T any] []T + +// NewJSONArray wraps an existing slice into a JSONArray. +func NewJSONArray[T any](a []T) JSONArray[T] { + return JSONArray[T](a) +} + +// Scan implements the [database/sql.Scanner] interface. +func (a *JSONArray[T]) Scan(src any) error { + if src == nil { + *a = nil + return nil + } + + bytes := src.([]byte) + if len(bytes) == 0 { + *a = nil + return nil + } + + return json.Unmarshal(bytes, a) +} + +// Value implements the [database/sql/driver.Valuer] interface. +func (a JSONArray[T]) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + return json.Marshal(a) +} diff --git a/internal/database/type_test.go b/internal/database/type_test.go index e56cdced76..7fab568a4e 100644 --- a/internal/database/type_test.go +++ b/internal/database/type_test.go @@ -5,6 +5,7 @@ import ( "database/sql/driver" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -452,3 +453,89 @@ func TestDuration_Scan(t *testing.T) { }) } } + +func TestJSONArray_Scan(t *testing.T) { + type args struct { + src any + } + tests := []struct { + name string + args args + want *JSONArray[string] + wantErr bool + }{ + { + name: "nil", + args: args{src: nil}, + want: new(JSONArray[string]), + wantErr: false, + }, + { + name: "zero bytes", + args: args{src: []byte("")}, + want: new(JSONArray[string]), + wantErr: false, + }, + { + name: "empty", + args: args{src: []byte("[]")}, + want: gu.Ptr(JSONArray[string]{}), + wantErr: false, + }, + { + name: "ok", + args: args{src: []byte("[\"a\", \"b\"]")}, + want: gu.Ptr(JSONArray[string]{"a", "b"}), + wantErr: false, + }, + { + name: "json error", + args: args{src: []byte("{\"a\": \"b\"}")}, + want: new(JSONArray[string]), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := new(JSONArray[string]) + err := got.Scan(tt.args.src) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestJSONArray_Value(t *testing.T) { + tests := []struct { + name string + a []string + want driver.Value + }{ + { + name: "nil", + a: nil, + want: nil, + }, + { + name: "empty", + a: []string{}, + want: []byte("[]"), + }, + { + name: "ok", + a: []string{"a", "b"}, + want: []byte("[\"a\",\"b\"]"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewJSONArray(tt.a).Value() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/domain/action.go b/internal/domain/action.go index 18dd23e8c5..b57dde6289 100644 --- a/internal/domain/action.go +++ b/internal/domain/action.go @@ -1,6 +1,7 @@ package domain import ( + "slices" "time" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -45,3 +46,51 @@ const ( ActionsMaxAllowed ActionsAllowedUnlimited ) + +type ActionFunction int32 + +const ( + ActionFunctionUnspecified ActionFunction = iota + ActionFunctionPreUserinfo + ActionFunctionPreAccessToken + ActionFunctionPreSAMLResponse + actionFunctionCount +) + +func (s ActionFunction) Valid() bool { + return s >= 0 && s < actionFunctionCount +} + +func (s ActionFunction) LocalizationKey() string { + if !s.Valid() { + return ActionFunctionUnspecified.LocalizationKey() + } + + switch s { + case ActionFunctionPreUserinfo: + return "preuserinfo" + case ActionFunctionPreAccessToken: + return "preaccesstoken" + case ActionFunctionPreSAMLResponse: + return "presamlresponse" + case ActionFunctionUnspecified, actionFunctionCount: + fallthrough + default: + return "unspecified" + } +} + +func AllActionFunctions() []string { + return []string{ + ActionFunctionPreUserinfo.LocalizationKey(), + ActionFunctionPreAccessToken.LocalizationKey(), + ActionFunctionPreSAMLResponse.LocalizationKey(), + } +} + +func ActionFunctionExists() func(string) bool { + functions := AllActionFunctions() + return func(s string) bool { + return slices.Contains(functions, s) + } +} diff --git a/internal/domain/application_saml.go b/internal/domain/application_saml.go index b00366df7e..de7ef789ee 100644 --- a/internal/domain/application_saml.go +++ b/internal/domain/application_saml.go @@ -7,11 +7,13 @@ import ( type SAMLApp struct { models.ObjectRoot - AppID string - AppName string - EntityID string - Metadata []byte - MetadataURL string + AppID string + AppName string + EntityID string + Metadata []byte + MetadataURL string + LoginVersion LoginVersion + LoginBaseURI string State AppState } diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index 85ec340f67..c880c5159e 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -43,6 +43,7 @@ type AuthRequest struct { ApplicationResourceOwner string PrivateLabelingSetting PrivateLabelingSetting SelectedIDPConfigID string + SelectedIDPConfigArgs map[string]any LinkingUsers []*ExternalUser PossibleSteps []NextStep `json:"-"` PasswordVerified bool diff --git a/internal/domain/flow.go b/internal/domain/flow.go index 143ce6bd0b..39cb13fc1e 100644 --- a/internal/domain/flow.go +++ b/internal/domain/flow.go @@ -1,7 +1,6 @@ package domain import ( - "slices" "strconv" ) @@ -150,20 +149,3 @@ func (s TriggerType) LocalizationKey() string { return "Action.TriggerType.Unspecified" } } - -func AllFunctions() []string { - functions := make([]string, 0) - for _, flowType := range AllFlowTypes() { - for _, triggerType := range flowType.TriggerTypes() { - functions = append(functions, flowType.LocalizationKey()+"."+triggerType.LocalizationKey()) - } - } - return functions -} - -func FunctionExists() func(string) bool { - functions := AllFunctions() - return func(s string) bool { - return slices.Contains(functions, s) - } -} diff --git a/internal/domain/request.go b/internal/domain/request.go index 1b54cfa41c..92e45c0d2f 100644 --- a/internal/domain/request.go +++ b/internal/domain/request.go @@ -62,11 +62,13 @@ func (a *AuthRequestSAML) IsValid() bool { } type AuthRequestDevice struct { - ClientID string - DeviceCode string - UserCode string - Scopes []string - Audience []string + ClientID string + DeviceCode string + UserCode string + Scopes []string + Audience []string + AppName string + ProjectName string } func (*AuthRequestDevice) Type() AuthRequestType { diff --git a/internal/eventstore/aggregate.go b/internal/eventstore/aggregate.go index 87282e9007..6c9157b7eb 100644 --- a/internal/eventstore/aggregate.go +++ b/internal/eventstore/aggregate.go @@ -78,15 +78,15 @@ func AggregateFromWriteModelCtx( // Aggregate is the basic implementation of Aggregater type Aggregate struct { // ID is the unique identitfier of this aggregate - ID string `json:"-"` + ID string `json:"id"` // Type is the name of the aggregate. - Type AggregateType `json:"-"` + Type AggregateType `json:"type"` // ResourceOwner is the org this aggregates belongs to - ResourceOwner string `json:"-"` + ResourceOwner string `json:"resourceOwner"` // InstanceID is the instance this aggregate belongs to - InstanceID string `json:"-"` + InstanceID string `json:"instanceId"` // Version is the semver this aggregate represents - Version Version `json:"-"` + Version Version `json:"version"` } // AggregateType is the object name diff --git a/internal/eventstore/event_base.go b/internal/eventstore/event_base.go index 45706641d8..ed81e95320 100644 --- a/internal/eventstore/event_base.go +++ b/internal/eventstore/event_base.go @@ -22,7 +22,7 @@ type BaseEvent struct { ID string EventType EventType `json:"-"` - Agg *Aggregate + Agg *Aggregate `json:"-"` Seq uint64 Pos float64 diff --git a/internal/eventstore/eventstore_pusher_test.go b/internal/eventstore/eventstore_pusher_test.go index 4e8e663667..318cf1a37e 100644 --- a/internal/eventstore/eventstore_pusher_test.go +++ b/internal/eventstore/eventstore_pusher_test.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" ) -func TestCRDB_Push_OneAggregate(t *testing.T) { +func TestEventstore_Push_OneAggregate(t *testing.T) { type args struct { ctx context.Context commands []eventstore.Command @@ -202,7 +202,7 @@ func TestCRDB_Push_OneAggregate(t *testing.T) { } } if _, err := db.Push(tt.args.ctx, tt.args.commands...); (err != nil) != tt.res.wantErr { - t.Errorf("CRDB.Push() error = %v, wantErr %v", err, tt.res.wantErr) + t.Errorf("eventstore.Push() error = %v, wantErr %v", err, tt.res.wantErr) } assertEventCount(t, @@ -218,7 +218,7 @@ func TestCRDB_Push_OneAggregate(t *testing.T) { } } -func TestCRDB_Push_MultipleAggregate(t *testing.T) { +func TestEventstore_Push_MultipleAggregate(t *testing.T) { type args struct { commands []eventstore.Command } @@ -312,7 +312,7 @@ func TestCRDB_Push_MultipleAggregate(t *testing.T) { }, ) if _, err := db.Push(context.Background(), tt.args.commands...); (err != nil) != tt.res.wantErr { - t.Errorf("CRDB.Push() error = %v, wantErr %v", err, tt.res.wantErr) + t.Errorf("eventstore.Push() error = %v, wantErr %v", err, tt.res.wantErr) } assertEventCount(t, clients[pusherName], tt.res.eventsRes.aggType, tt.res.eventsRes.aggID, tt.res.eventsRes.pushedEventsCount) @@ -321,7 +321,7 @@ func TestCRDB_Push_MultipleAggregate(t *testing.T) { } } -func TestCRDB_Push_Parallel(t *testing.T) { +func TestEventstore_Push_Parallel(t *testing.T) { type args struct { commands [][]eventstore.Command } @@ -453,7 +453,7 @@ func TestCRDB_Push_Parallel(t *testing.T) { } } -func TestCRDB_Push_ResourceOwner(t *testing.T) { +func TestEventstore_Push_ResourceOwner(t *testing.T) { type args struct { commands []eventstore.Command } @@ -587,7 +587,7 @@ func TestCRDB_Push_ResourceOwner(t *testing.T) { events, err := db.Push(context.Background(), tt.args.commands...) if err != nil { - t.Errorf("CRDB.Push() error = %v", err) + t.Errorf("eventstore.Push() error = %v", err) } if len(events) != len(tt.res.resourceOwners) { diff --git a/internal/eventstore/eventstore_querier_test.go b/internal/eventstore/eventstore_querier_test.go index 4b7ad78b25..3f23c5da75 100644 --- a/internal/eventstore/eventstore_querier_test.go +++ b/internal/eventstore/eventstore_querier_test.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" ) -func TestCRDB_Filter(t *testing.T) { +func TestEventstore_Filter(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -120,18 +120,18 @@ func TestCRDB_Filter(t *testing.T) { events, err := db.Filter(context.Background(), tt.args.searchQuery) if (err != nil) != tt.wantErr { - t.Errorf("CRDB.query() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } if len(events) != tt.res.eventCount { - t.Errorf("CRDB.query() expected event count: %d got %d", tt.res.eventCount, len(events)) + t.Errorf("eventstore.query() expected event count: %d got %d", tt.res.eventCount, len(events)) } }) } } } -func TestCRDB_LatestSequence(t *testing.T) { +func TestEventstore_LatestSequence(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -204,10 +204,10 @@ func TestCRDB_LatestSequence(t *testing.T) { sequence, err := db.LatestSequence(context.Background(), tt.args.searchQuery) if (err != nil) != tt.wantErr { - t.Errorf("CRDB.query() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } if tt.res.sequence > sequence { - t.Errorf("CRDB.query() expected sequence: %v got %v", tt.res.sequence, sequence) + t.Errorf("eventstore.query() expected sequence: %v got %v", tt.res.sequence, sequence) } }) } diff --git a/internal/eventstore/example_test.go b/internal/eventstore/example_test.go index ee053d16da..2b6c205ddd 100644 --- a/internal/eventstore/example_test.go +++ b/internal/eventstore/example_test.go @@ -289,8 +289,8 @@ func (rm *UserReadModel) Reduce() error { func TestUserReadModel(t *testing.T) { es := eventstore.NewEventstore( &eventstore.Config{ - Querier: query_repo.NewCRDB(testCRDBClient), - Pusher: v3.NewEventstore(testCRDBClient), + Querier: query_repo.NewPostgres(testClient), + Pusher: v3.NewEventstore(testClient), }, ) diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go index 2c2f88f8a0..43c3e58b3b 100644 --- a/internal/eventstore/handler/v2/handler.go +++ b/internal/eventstore/handler/v2/handler.go @@ -67,6 +67,8 @@ type Handler struct { cacheInvalidations []func(ctx context.Context, aggregates []*eventstore.Aggregate) queryInstances func() ([]string, error) + + metrics *ProjectionMetrics } var _ migration.Migration = (*Handler)(nil) @@ -159,6 +161,8 @@ func NewHandler( aggregates[reducer.Aggregate] = eventTypes } + metrics := NewProjectionMetrics() + handler := &Handler{ projection: projection, client: config.Client, @@ -178,6 +182,7 @@ func NewHandler( } return nil, nil }, + metrics: metrics, } return handler @@ -263,12 +268,16 @@ func (h *Handler) triggerInstances(ctx context.Context, instances []string, trig // simple implementation of do while _, err := h.Trigger(instanceCtx, triggerOpts...) - h.log().WithField("instance", instance).OnError(err).Debug("trigger failed") + // skip retry if everything is fine + if err == nil { + continue + } + h.log().WithField("instance", instance).WithError(err).Debug("trigger failed") time.Sleep(h.retryFailedAfter) // retry if trigger failed for ; err != nil; _, err = h.Trigger(instanceCtx, triggerOpts...) { time.Sleep(h.retryFailedAfter) - h.log().WithField("instance", instance).OnError(err).Debug("trigger failed") + h.log().WithField("instance", instance).WithError(err).Debug("trigger failed") } } } @@ -479,6 +488,8 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add defer cancel() } + start := time.Now() + tx, err := h.client.BeginTx(txCtx, nil) if err != nil { return false, err @@ -498,7 +509,7 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add } return additionalIteration, err } - // stop execution if currentState.eventTimestamp >= config.maxCreatedAt + // stop execution if currentState.position >= config.maxPosition if config.maxPosition != 0 && currentState.position >= config.maxPosition { return false, nil } @@ -514,7 +525,14 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add if err == nil { err = commitErr } + + h.metrics.ProjectionEventsProcessed(ctx, h.ProjectionName(), int64(len(statements)), err == nil) + if err == nil && currentState.aggregateID != "" && len(statements) > 0 { + // Don't update projection timing or latency unless we successfully processed events + h.metrics.ProjectionUpdateTiming(ctx, h.ProjectionName(), float64(time.Since(start).Seconds())) + h.metrics.ProjectionStateLatency(ctx, h.ProjectionName(), time.Since(currentState.eventTimestamp).Seconds()) + h.invalidateCaches(ctx, aggregatesFromStatements(statements)) } }() @@ -536,6 +554,7 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add currentState.aggregateType = statements[lastProcessedIndex].Aggregate.Type currentState.sequence = statements[lastProcessedIndex].Sequence currentState.eventTimestamp = statements[lastProcessedIndex].CreationDate + err = h.setState(tx, currentState) return additionalIteration, err @@ -646,7 +665,6 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder builder := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AwaitOpenTransactions(). Limit(uint64(h.bulkLimit)). - AllowTimeTravel(). OrderAsc(). InstanceID(currentState.instanceID) @@ -658,15 +676,15 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder } } - for aggregateType, eventTypes := range h.eventTypes { - builder = builder. - AddQuery(). - AggregateTypes(aggregateType). - EventTypes(eventTypes...). - Builder() + aggregateTypes := make([]eventstore.AggregateType, 0, len(h.eventTypes)) + eventTypes := make([]eventstore.EventType, 0, len(h.eventTypes)) + + for aggregate, events := range h.eventTypes { + aggregateTypes = append(aggregateTypes, aggregate) + eventTypes = append(eventTypes, events...) } - return builder + return builder.AddQuery().AggregateTypes(aggregateTypes...).EventTypes(eventTypes...).Builder() } // ProjectionName returns the name of the underlying projection. diff --git a/internal/eventstore/handler/v2/metrics.go b/internal/eventstore/handler/v2/metrics.go new file mode 100644 index 0000000000..6876bb3aa4 --- /dev/null +++ b/internal/eventstore/handler/v2/metrics.go @@ -0,0 +1,70 @@ +package handler + +import ( + "context" + + "github.com/zitadel/logging" + "go.opentelemetry.io/otel/attribute" + + "github.com/zitadel/zitadel/internal/telemetry/metrics" +) + +const ( + ProjectionLabel = "projection" + SuccessLabel = "success" + + ProjectionEventsProcessed = "projection_events_processed" + ProjectionHandleTimerMetric = "projection_handle_timer" + ProjectionStateLatencyMetric = "projection_state_latency" +) + +type ProjectionMetrics struct { + provider metrics.Metrics +} + +func NewProjectionMetrics() *ProjectionMetrics { + projectionMetrics := &ProjectionMetrics{provider: metrics.M} + + err := projectionMetrics.provider.RegisterCounter( + ProjectionEventsProcessed, + "Number of events reduced to process projection updates", + ) + logging.OnError(err).Error("failed to register projection events processed counter") + err = projectionMetrics.provider.RegisterHistogram( + ProjectionHandleTimerMetric, + "Time taken to process a projection update", + "s", + []float64{0.005, 0.01, 0.05, 0.1, 1, 5, 10, 30, 60, 120}, + ) + logging.OnError(err).Error("failed to register projection handle timer metric") + err = projectionMetrics.provider.RegisterHistogram( + ProjectionStateLatencyMetric, + "When finishing processing a batch of events, this track the age of the last events seen from current time", + "s", + []float64{0.1, 0.5, 1, 5, 10, 30, 60, 300, 600, 1800}, + ) + logging.OnError(err).Error("failed to register projection state latency metric") + return projectionMetrics +} + +func (m *ProjectionMetrics) ProjectionUpdateTiming(ctx context.Context, projection string, duration float64) { + err := m.provider.AddHistogramMeasurement(ctx, ProjectionHandleTimerMetric, duration, map[string]attribute.Value{ + ProjectionLabel: attribute.StringValue(projection), + }) + logging.OnError(err).Error("failed to add projection trigger timing") +} + +func (m *ProjectionMetrics) ProjectionEventsProcessed(ctx context.Context, projection string, count int64, success bool) { + err := m.provider.AddCount(ctx, ProjectionEventsProcessed, count, map[string]attribute.Value{ + ProjectionLabel: attribute.StringValue(projection), + SuccessLabel: attribute.BoolValue(success), + }) + logging.OnError(err).Error("failed to add projection events processed metric") +} + +func (m *ProjectionMetrics) ProjectionStateLatency(ctx context.Context, projection string, latency float64) { + err := m.provider.AddHistogramMeasurement(ctx, ProjectionStateLatencyMetric, latency, map[string]attribute.Value{ + ProjectionLabel: attribute.StringValue(projection), + }) + logging.OnError(err).Error("failed to add projection state latency metric") +} diff --git a/internal/eventstore/handler/v2/metrics_test.go b/internal/eventstore/handler/v2/metrics_test.go new file mode 100644 index 0000000000..54e7623462 --- /dev/null +++ b/internal/eventstore/handler/v2/metrics_test.go @@ -0,0 +1,132 @@ +package handler + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/telemetry/metrics" +) + +func TestNewProjectionMetrics(t *testing.T) { + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + + metrics := NewProjectionMetrics() + require.NotNil(t, metrics) + assert.NotNil(t, metrics.provider) +} + +func TestProjectionMetrics_ProjectionUpdateTiming(t *testing.T) { + + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + projectionMetrics := NewProjectionMetrics() + + ctx := context.Background() + projection := "test_projection" + duration := 0.5 + + projectionMetrics.ProjectionUpdateTiming(ctx, projection, duration) + + values := mockMetrics.GetHistogramValues(ProjectionHandleTimerMetric) + require.Len(t, values, 1) + assert.Equal(t, duration, values[0]) + + labels := mockMetrics.GetHistogramLabels(ProjectionHandleTimerMetric) + require.Len(t, labels, 1) + assert.Equal(t, projection, labels[0][ProjectionLabel].AsString()) +} + +func TestProjectionMetrics_ProjectionEventsProcessed(t *testing.T) { + + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + projectionMetrics := NewProjectionMetrics() + + ctx := context.Background() + projection := "test_projection" + count := int64(5) + success := true + + projectionMetrics.ProjectionEventsProcessed(ctx, projection, count, success) + + value := mockMetrics.GetCounterValue(ProjectionEventsProcessed) + assert.Equal(t, count, value) + + labels := mockMetrics.GetCounterLabels(ProjectionEventsProcessed) + require.Len(t, labels, 1) + assert.Equal(t, projection, labels[0][ProjectionLabel].AsString()) + assert.Equal(t, success, labels[0][SuccessLabel].AsBool()) +} + +func TestProjectionMetrics_ProjectionStateLatency(t *testing.T) { + + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + projectionMetrics := NewProjectionMetrics() + + ctx := context.Background() + projection := "test_projection" + latency := 10.0 + + projectionMetrics.ProjectionStateLatency(ctx, projection, latency) + + values := mockMetrics.GetHistogramValues(ProjectionStateLatencyMetric) + require.Len(t, values, 1) + assert.Equal(t, latency, values[0]) + + labels := mockMetrics.GetHistogramLabels(ProjectionStateLatencyMetric) + require.Len(t, labels, 1) + assert.Equal(t, projection, labels[0][ProjectionLabel].AsString()) +} + +func TestProjectionMetrics_Integration(t *testing.T) { + + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + projectionMetrics := NewProjectionMetrics() + + ctx := context.Background() + projection := "test_projection" + + start := time.Now() + + projectionMetrics.ProjectionEventsProcessed(ctx, projection, 3, true) + projectionMetrics.ProjectionEventsProcessed(ctx, projection, 1, false) + + duration := time.Since(start).Seconds() + projectionMetrics.ProjectionUpdateTiming(ctx, projection, duration) + + latency := 5.0 + projectionMetrics.ProjectionStateLatency(ctx, projection, latency) + + value := mockMetrics.GetCounterValue(ProjectionEventsProcessed) + assert.Equal(t, int64(4), value) + + timingValues := mockMetrics.GetHistogramValues(ProjectionHandleTimerMetric) + require.Len(t, timingValues, 1) + assert.Equal(t, duration, timingValues[0]) + + latencyValues := mockMetrics.GetHistogramValues(ProjectionStateLatencyMetric) + require.Len(t, latencyValues, 1) + assert.Equal(t, latency, latencyValues[0]) + + eventsLabels := mockMetrics.GetCounterLabels(ProjectionEventsProcessed) + require.Len(t, eventsLabels, 2) + assert.Equal(t, projection, eventsLabels[0][ProjectionLabel].AsString()) + assert.Equal(t, true, eventsLabels[0][SuccessLabel].AsBool()) + assert.Equal(t, projection, eventsLabels[1][ProjectionLabel].AsString()) + assert.Equal(t, false, eventsLabels[1][SuccessLabel].AsBool()) + + timingLabels := mockMetrics.GetHistogramLabels(ProjectionHandleTimerMetric) + require.Len(t, timingLabels, 1) + assert.Equal(t, projection, timingLabels[0][ProjectionLabel].AsString()) + + latencyLabels := mockMetrics.GetHistogramLabels(ProjectionStateLatencyMetric) + require.Len(t, latencyLabels, 1) + assert.Equal(t, projection, latencyLabels[0][ProjectionLabel].AsString()) +} diff --git a/internal/eventstore/local_crdb_test.go b/internal/eventstore/local_postgres_test.go similarity index 63% rename from internal/eventstore/local_crdb_test.go rename to internal/eventstore/local_postgres_test.go index 87c5084fe7..d75292b3ff 100644 --- a/internal/eventstore/local_crdb_test.go +++ b/internal/eventstore/local_postgres_test.go @@ -8,111 +8,100 @@ import ( "testing" "time" - "github.com/cockroachdb/cockroach-go/v2/testserver" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/initialise" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/cockroach" + "github.com/zitadel/zitadel/internal/database/dialect" + "github.com/zitadel/zitadel/internal/database/postgres" "github.com/zitadel/zitadel/internal/eventstore" es_sql "github.com/zitadel/zitadel/internal/eventstore/repository/sql" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" ) var ( - testCRDBClient *database.DB - queriers map[string]eventstore.Querier = make(map[string]eventstore.Querier) - pushers map[string]eventstore.Pusher = make(map[string]eventstore.Pusher) - clients map[string]*database.DB = make(map[string]*database.DB) + testClient *database.DB + queriers map[string]eventstore.Querier = make(map[string]eventstore.Querier) + pushers map[string]eventstore.Pusher = make(map[string]eventstore.Pusher) + clients map[string]*database.DB = make(map[string]*database.DB) ) func TestMain(m *testing.M) { - opts := make([]testserver.TestServerOpt, 0, 1) - if version := os.Getenv("ZITADEL_CRDB_VERSION"); version != "" { - opts = append(opts, testserver.CustomVersionOpt(version)) - } - ts, err := testserver.NewTestServer(opts...) - if err != nil { - logging.WithFields("error", err).Fatal("unable to start db") - } + os.Exit(func() int { + config, cleanup := postgres.StartEmbedded() + defer cleanup() - testCRDBClient = &database.DB{ - Database: new(testDB), - } - - connConfig, err := pgxpool.ParseConfig(ts.PGURL().String()) - if err != nil { - logging.WithFields("error", err).Fatal("unable to parse db url") - } - connConfig.AfterConnect = new_es.RegisterEventstoreTypes - pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) - if err != nil { - logging.WithFields("error", err).Fatal("unable to create db pool") - } - testCRDBClient.DB = stdlib.OpenDBFromPool(pool) - if err = testCRDBClient.Ping(); err != nil { - logging.WithFields("error", err).Fatal("unable to ping db") - } - - v2 := &es_sql.CRDB{DB: testCRDBClient} - queriers["v2(inmemory)"] = v2 - clients["v2(inmemory)"] = testCRDBClient - - pushers["v3(inmemory)"] = new_es.NewEventstore(testCRDBClient) - clients["v3(inmemory)"] = testCRDBClient - - if localDB, err := connectLocalhost(); err == nil { - if err = initDB(context.Background(), localDB); err != nil { - logging.WithFields("error", err).Fatal("migrations failed") + testClient = &database.DB{ + Database: new(testDB), } - pushers["v3(singlenode)"] = new_es.NewEventstore(localDB) - clients["v3(singlenode)"] = localDB - } - // pushers["v2(inmemory)"] = v2 + connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) + logging.OnError(err).Fatal("unable to parse db url") - defer func() { - testCRDBClient.Close() - ts.Stop() - }() + connConfig.AfterConnect = new_es.RegisterEventstoreTypes + pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) + logging.OnError(err).Fatal("unable to create db pool") - if err = initDB(context.Background(), testCRDBClient); err != nil { - logging.WithFields("error", err).Fatal("migrations failed") - } + testClient.DB = stdlib.OpenDBFromPool(pool) + err = testClient.Ping() + logging.OnError(err).Fatal("unable to ping db") - os.Exit(m.Run()) + v2 := &es_sql.Postgres{DB: testClient} + queriers["v2(inmemory)"] = v2 + clients["v2(inmemory)"] = testClient + + pushers["v3(inmemory)"] = new_es.NewEventstore(testClient) + clients["v3(inmemory)"] = testClient + + if localDB, err := connectLocalhost(); err == nil { + err = initDB(context.Background(), localDB) + logging.OnError(err).Fatal("migrations failed") + + pushers["v3(singlenode)"] = new_es.NewEventstore(localDB) + clients["v3(singlenode)"] = localDB + } + + defer func() { + logging.OnError(testClient.Close()).Error("unable to close db") + }() + + err = initDB(context.Background(), &database.DB{DB: testClient.DB, Database: &postgres.Config{Database: "zitadel"}}) + logging.OnError(err).Fatal("migrations failed") + + return m.Run() + }()) } func initDB(ctx context.Context, db *database.DB) error { - initialise.ReadStmts("cockroach") config := new(database.Config) - config.SetConnector(&cockroach.Config{ - User: cockroach.User{ - Username: "zitadel", - }, - Database: "zitadel", - }) + config.SetConnector(&postgres.Config{User: postgres.User{Username: "zitadel"}, Database: "zitadel"}) + + if err := initialise.ReadStmts(); err != nil { + return err + } + err := initialise.Init(ctx, db, initialise.VerifyUser(config.Username(), ""), initialise.VerifyDatabase(config.DatabaseName()), - initialise.VerifyGrant(config.DatabaseName(), config.Username()), - initialise.VerifySettings(config.DatabaseName(), config.Username())) + initialise.VerifyGrant(config.DatabaseName(), config.Username())) if err != nil { return err } + err = initialise.VerifyZitadel(ctx, db, *config) if err != nil { return err } + // create old events _, err = db.Exec(oldEventsTable) return err } func connectLocalhost() (*database.DB, error) { - client, err := sql.Open("pgx", "postgresql://root@localhost:26257/defaultdb?sslmode=disable") + client, err := sql.Open("pgx", "postgresql://postgres@localhost:5432/postgres?sslmode=disable") if err != nil { return nil, err } @@ -134,7 +123,7 @@ func (*testDB) DatabaseName() string { return "db" } func (*testDB) Username() string { return "user" } -func (*testDB) Type() string { return "cockroach" } +func (*testDB) Type() dialect.DatabaseType { return dialect.DatabaseTypePostgres } func generateCommand(aggregateType eventstore.AggregateType, aggregateID string, opts ...func(*testEvent)) eventstore.Command { e := &testEvent{ @@ -177,7 +166,7 @@ func canceledCtx() context.Context { } func fillUniqueData(unique_type, field, instanceID string) error { - _, err := testCRDBClient.Exec("INSERT INTO eventstore.unique_constraints (unique_type, unique_field, instance_id) VALUES ($1, $2, $3)", unique_type, field, instanceID) + _, err := testClient.Exec("INSERT INTO eventstore.unique_constraints (unique_type, unique_field, instance_id) VALUES ($1, $2, $3)", unique_type, field, instanceID) return err } @@ -251,5 +240,5 @@ const oldEventsTable = `CREATE TABLE IF NOT EXISTS eventstore.events ( , "position" DECIMAL NOT NULL , in_tx_order INTEGER NOT NULL - , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, event_sequence DESC) + , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, event_sequence) );` diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index 9ae0b6b1ea..ced76953cb 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -144,7 +144,17 @@ func (m *MockRepository) ExpectPushFailed(err error, expectedCommands []eventsto assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Creator(), commands[i].Creator()) assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Type(), commands[i].Type()) assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Revision(), commands[i].Revision()) - assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Payload(), commands[i].Payload()) + var expectedPayload []byte + expectedPayload, ok := expectedCommand.Payload().([]byte) + if !ok { + expectedPayload, _ = json.Marshal(expectedCommand.Payload()) + } + if string(expectedPayload) == "" { + expectedPayload = []byte("null") + } + gotPayload, _ := json.Marshal(commands[i].Payload()) + + assert.Equal(m.MockPusher.ctrl.T, expectedPayload, gotPayload) assert.ElementsMatch(m.MockPusher.ctrl.T, expectedCommand.UniqueConstraints(), commands[i].UniqueConstraints()) } diff --git a/internal/eventstore/repository/search_query.go b/internal/eventstore/repository/search_query.go index f84c7f1201..6ffba31ca8 100644 --- a/internal/eventstore/repository/search_query.go +++ b/internal/eventstore/repository/search_query.go @@ -16,7 +16,6 @@ type SearchQuery struct { Tx *sql.Tx LockRows bool LockOption eventstore.LockOption - AllowTimeTravel bool AwaitOpenTransactions bool Limit uint64 Offset uint32 @@ -51,11 +50,11 @@ const ( OperationGreater // OperationLess compares if the given values is less than the stored one OperationLess - //OperationIn checks if a stored value matches one of the passed value list + // OperationIn checks if a stored value matches one of the passed value list OperationIn - //OperationJSONContains checks if a stored value matches the given json + // OperationJSONContains checks if a stored value matches the given json OperationJSONContains - //OperationNotIn checks if a stored value does not match one of the passed value list + // OperationNotIn checks if a stored value does not match one of the passed value list OperationNotIn operationCount @@ -65,25 +64,25 @@ const ( type Field int32 const ( - //FieldAggregateType represents the aggregate type field + // FieldAggregateType represents the aggregate type field FieldAggregateType Field = iota + 1 - //FieldAggregateID represents the aggregate id field + // FieldAggregateID represents the aggregate id field FieldAggregateID - //FieldSequence represents the sequence field + // FieldSequence represents the sequence field FieldSequence - //FieldResourceOwner represents the resource owner field + // FieldResourceOwner represents the resource owner field FieldResourceOwner - //FieldInstanceID represents the instance id field + // FieldInstanceID represents the instance id field FieldInstanceID - //FieldEditorService represents the editor service field + // FieldEditorService represents the editor service field FieldEditorService - //FieldEditorUser represents the editor user field + // FieldEditorUser represents the editor user field FieldEditorUser - //FieldEventType represents the event type field + // FieldEventType represents the event type field FieldEventType - //FieldEventData represents the event data field + // FieldEventData represents the event data field FieldEventData - //FieldCreationDate represents the creation date field + // FieldCreationDate represents the creation date field FieldCreationDate // FieldPosition represents the field of the global sequence FieldPosition @@ -129,7 +128,6 @@ func QueryFromBuilder(builder *eventstore.SearchQueryBuilder) (*SearchQuery, err Offset: builder.GetOffset(), Desc: builder.GetDesc(), Tx: builder.GetTx(), - AllowTimeTravel: builder.GetAllowTimeTravel(), AwaitOpenTransactions: builder.GetAwaitOpenTransactions(), SubQueries: make([][]*Filter, len(builder.GetQueries())), } diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go deleted file mode 100644 index 68610676c3..0000000000 --- a/internal/eventstore/repository/sql/crdb.go +++ /dev/null @@ -1,455 +0,0 @@ -package sql - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "regexp" - "strconv" - "strings" - - "github.com/cockroachdb/cockroach-go/v2/crdb" - "github.com/jackc/pgx/v5/pgconn" - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/repository" - "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" -) - -const ( - //as soon as stored procedures are possible in crdb - // we could move the code to migrations and call the procedure - // traking issue: https://github.com/cockroachdb/cockroach/issues/17511 - // - //previous_data selects the needed data of the latest event of the aggregate - // and buffers it (crdb inmemory) - crdbInsert = "WITH previous_data (aggregate_type_sequence, aggregate_sequence, resource_owner) AS (" + - "SELECT agg_type.seq, agg.seq, agg.ro FROM " + - "(" + - //max sequence of requested aggregate type - " SELECT MAX(event_sequence) seq, 1 join_me" + - " FROM eventstore.events" + - " WHERE aggregate_type = $2" + - " AND (CASE WHEN $9::TEXT IS NULL THEN instance_id is null else instance_id = $9::TEXT END)" + - ") AS agg_type " + - // combined with - "LEFT JOIN " + - "(" + - // max sequence and resource owner of aggregate root - " SELECT event_sequence seq, resource_owner ro, 1 join_me" + - " FROM eventstore.events" + - " WHERE aggregate_type = $2 AND aggregate_id = $3" + - " AND (CASE WHEN $9::TEXT IS NULL THEN instance_id is null else instance_id = $9::TEXT END)" + - " ORDER BY event_sequence DESC" + - " LIMIT 1" + - ") AS agg USING(join_me)" + - ") " + - "INSERT INTO eventstore.events (" + - " event_type," + - " aggregate_type," + - " aggregate_id," + - " aggregate_version," + - " creation_date," + - " position," + - " event_data," + - " editor_user," + - " editor_service," + - " resource_owner," + - " instance_id," + - " event_sequence," + - " previous_aggregate_sequence," + - " previous_aggregate_type_sequence," + - " in_tx_order" + - ") " + - // defines the data to be inserted - "SELECT" + - " $1::VARCHAR AS event_type," + - " $2::VARCHAR AS aggregate_type," + - " $3::VARCHAR AS aggregate_id," + - " $4::VARCHAR AS aggregate_version," + - " hlc_to_timestamp(cluster_logical_timestamp()) AS creation_date," + - " cluster_logical_timestamp() AS position," + - " $5::JSONB AS event_data," + - " $6::VARCHAR AS editor_user," + - " $7::VARCHAR AS editor_service," + - " COALESCE((resource_owner), $8::VARCHAR) AS resource_owner," + - " $9::VARCHAR AS instance_id," + - " COALESCE(aggregate_sequence, 0)+1," + - " aggregate_sequence AS previous_aggregate_sequence," + - " aggregate_type_sequence AS previous_aggregate_type_sequence," + - " $10 AS in_tx_order " + - "FROM previous_data " + - "RETURNING id, event_sequence, creation_date, resource_owner, instance_id" - - uniqueInsert = `INSERT INTO eventstore.unique_constraints - ( - unique_type, - unique_field, - instance_id - ) - VALUES ( - $1, - $2, - $3 - )` - - uniqueDelete = `DELETE FROM eventstore.unique_constraints - WHERE unique_type = $1 and unique_field = $2 and instance_id = $3` - uniqueDeleteInstance = `DELETE FROM eventstore.unique_constraints - WHERE instance_id = $1` -) - -// awaitOpenTransactions ensures event ordering, so we don't events younger that open transactions -var ( - awaitOpenTransactionsV1 string - awaitOpenTransactionsV2 string -) - -func awaitOpenTransactions(useV1 bool) string { - if useV1 { - return awaitOpenTransactionsV1 - } - return awaitOpenTransactionsV2 -} - -type CRDB struct { - *database.DB -} - -func NewCRDB(client *database.DB) *CRDB { - switch client.Type() { - case "cockroach": - awaitOpenTransactionsV1 = " AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))" - awaitOpenTransactionsV2 = ` AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))` - case "postgres": - awaitOpenTransactionsV1 = ` AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` - awaitOpenTransactionsV2 = ` AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` - } - - return &CRDB{client} -} - -func (db *CRDB) Health(ctx context.Context) error { return db.Ping() } - -// Push adds all events to the eventstreams of the aggregates. -// This call is transaction save. The transaction will be rolled back if one event fails -func (db *CRDB) Push(ctx context.Context, commands ...eventstore.Command) (events []eventstore.Event, err error) { - events = make([]eventstore.Event, len(commands)) - - err = crdb.ExecuteTx(ctx, db.DB.DB, nil, func(tx *sql.Tx) error { - - var uniqueConstraints []*eventstore.UniqueConstraint - - for i, command := range commands { - if command.Aggregate().InstanceID == "" { - command.Aggregate().InstanceID = authz.GetInstance(ctx).InstanceID() - } - - var payload []byte - if command.Payload() != nil { - payload, err = json.Marshal(command.Payload()) - if err != nil { - return err - } - } - e := &repository.Event{ - Typ: command.Type(), - Data: payload, - EditorUser: command.Creator(), - Version: command.Aggregate().Version, - AggregateID: command.Aggregate().ID, - AggregateType: command.Aggregate().Type, - ResourceOwner: sql.NullString{String: command.Aggregate().ResourceOwner, Valid: command.Aggregate().ResourceOwner != ""}, - InstanceID: command.Aggregate().InstanceID, - } - - err := tx.QueryRowContext(ctx, crdbInsert, - e.Type(), - e.Aggregate().Type, - e.Aggregate().ID, - e.Aggregate().Version, - payload, - e.Creator(), - "zitadel", - e.Aggregate().ResourceOwner, - e.Aggregate().InstanceID, - i, - ).Scan(&e.ID, &e.Seq, &e.CreationDate, &e.ResourceOwner, &e.InstanceID) - - if err != nil { - logging.WithFields( - "aggregate", e.Aggregate().Type, - "aggregateId", e.Aggregate().ID, - "aggregateType", e.Aggregate().Type, - "eventType", e.Type(), - "instanceID", e.Aggregate().InstanceID, - ).WithError(err).Debug("query failed") - return zerrors.ThrowInternal(err, "SQL-SBP37", "unable to create event") - } - - uniqueConstraints = append(uniqueConstraints, command.UniqueConstraints()...) - events[i] = e - } - - return db.handleUniqueConstraints(ctx, tx, uniqueConstraints...) - }) - if err != nil && !errors.Is(err, &zerrors.ZitadelError{}) { - err = zerrors.ThrowInternal(err, "SQL-DjgtG", "unable to store events") - } - - return events, err -} - -// handleUniqueConstraints adds or removes unique constraints -func (db *CRDB) handleUniqueConstraints(ctx context.Context, tx *sql.Tx, uniqueConstraints ...*eventstore.UniqueConstraint) (err error) { - if len(uniqueConstraints) == 0 || (len(uniqueConstraints) == 1 && uniqueConstraints[0] == nil) { - return nil - } - - for _, uniqueConstraint := range uniqueConstraints { - uniqueConstraint.UniqueField = strings.ToLower(uniqueConstraint.UniqueField) - switch uniqueConstraint.Action { - case eventstore.UniqueConstraintAdd: - _, err := tx.ExecContext(ctx, uniqueInsert, uniqueConstraint.UniqueType, uniqueConstraint.UniqueField, authz.GetInstance(ctx).InstanceID()) - if err != nil { - logging.WithFields( - "unique_type", uniqueConstraint.UniqueType, - "unique_field", uniqueConstraint.UniqueField).WithError(err).Info("insert unique constraint failed") - - if db.isUniqueViolationError(err) { - return zerrors.ThrowAlreadyExists(err, "SQL-wHcEq", uniqueConstraint.ErrorMessage) - } - - return zerrors.ThrowInternal(err, "SQL-dM9ds", "unable to create unique constraint") - } - case eventstore.UniqueConstraintRemove: - _, err := tx.ExecContext(ctx, uniqueDelete, uniqueConstraint.UniqueType, uniqueConstraint.UniqueField, authz.GetInstance(ctx).InstanceID()) - if err != nil { - logging.WithFields( - "unique_type", uniqueConstraint.UniqueType, - "unique_field", uniqueConstraint.UniqueField).WithError(err).Info("delete unique constraint failed") - return zerrors.ThrowInternal(err, "SQL-6n88i", "unable to remove unique constraint") - } - case eventstore.UniqueConstraintInstanceRemove: - _, err := tx.ExecContext(ctx, uniqueDeleteInstance, authz.GetInstance(ctx).InstanceID()) - if err != nil { - logging.WithFields( - "instance_id", authz.GetInstance(ctx).InstanceID()).WithError(err).Info("delete instance unique constraints failed") - return zerrors.ThrowInternal(err, "SQL-6n88i", "unable to remove unique constraints of instance") - } - } - } - return nil -} - -// FilterToReducer finds all events matching the given search query and passes them to the reduce function. -func (crdb *CRDB) FilterToReducer(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder, reduce eventstore.Reducer) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - err = query(ctx, crdb, searchQuery, reduce, false) - if err == nil { - return nil - } - pgErr := new(pgconn.PgError) - // check events2 not exists - if errors.As(err, &pgErr) && pgErr.Code == "42P01" { - return query(ctx, crdb, searchQuery, reduce, true) - } - return err -} - -// LatestSequence returns the latest sequence found by the search query -func (db *CRDB) LatestSequence(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (float64, error) { - var position sql.NullFloat64 - err := query(ctx, db, searchQuery, &position, false) - return position.Float64, err -} - -// InstanceIDs returns the instance ids found by the search query -func (db *CRDB) InstanceIDs(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) ([]string, error) { - var ids []string - err := query(ctx, db, searchQuery, &ids, false) - if err != nil { - return nil, err - } - return ids, nil -} - -func (db *CRDB) Client() *database.DB { - return db.DB -} - -func (db *CRDB) orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string { - if useV1 { - if desc { - return ` ORDER BY event_sequence DESC` - } - return ` ORDER BY event_sequence` - } - if shouldOrderBySequence { - if desc { - return ` ORDER BY "sequence" DESC` - } - return ` ORDER BY "sequence"` - } - - if desc { - return ` ORDER BY "position" DESC, in_tx_order DESC` - } - return ` ORDER BY "position", in_tx_order` -} - -func (db *CRDB) eventQuery(useV1 bool) string { - if useV1 { - return "SELECT" + - " creation_date" + - ", event_type" + - ", event_sequence" + - ", event_data" + - ", editor_user" + - ", resource_owner" + - ", instance_id" + - ", aggregate_type" + - ", aggregate_id" + - ", aggregate_version" + - " FROM eventstore.events" - } - return "SELECT" + - " created_at" + - ", event_type" + - `, "sequence"` + - `, "position"` + - ", payload" + - ", creator" + - `, "owner"` + - ", instance_id" + - ", aggregate_type" + - ", aggregate_id" + - ", revision" + - " FROM eventstore.events2" -} - -func (db *CRDB) maxSequenceQuery(useV1 bool) string { - if useV1 { - return `SELECT event_sequence FROM eventstore.events` - } - return `SELECT "position" FROM eventstore.events2` -} - -func (db *CRDB) instanceIDsQuery(useV1 bool) string { - table := "eventstore.events2" - if useV1 { - table = "eventstore.events" - } - return "SELECT DISTINCT instance_id FROM " + table -} - -func (db *CRDB) columnName(col repository.Field, useV1 bool) string { - switch col { - case repository.FieldAggregateID: - return "aggregate_id" - case repository.FieldAggregateType: - return "aggregate_type" - case repository.FieldSequence: - if useV1 { - return "event_sequence" - } - return `"sequence"` - case repository.FieldResourceOwner: - if useV1 { - return "resource_owner" - } - return `"owner"` - case repository.FieldInstanceID: - return "instance_id" - case repository.FieldEditorService: - if useV1 { - return "editor_service" - } - return "" - case repository.FieldEditorUser: - if useV1 { - return "editor_user" - } - return "creator" - case repository.FieldEventType: - return "event_type" - case repository.FieldEventData: - if useV1 { - return "event_data" - } - return "payload" - case repository.FieldCreationDate: - if useV1 { - return "creation_date" - } - return "created_at" - case repository.FieldPosition: - return `"position"` - default: - return "" - } -} - -func (db *CRDB) conditionFormat(operation repository.Operation) string { - switch operation { - case repository.OperationIn: - return "%s %s ANY(?)" - case repository.OperationNotIn: - return "%s %s ALL(?)" - } - return "%s %s ?" -} - -func (db *CRDB) operation(operation repository.Operation) string { - switch operation { - case repository.OperationEquals, repository.OperationIn: - return "=" - case repository.OperationGreater: - return ">" - case repository.OperationLess: - return "<" - case repository.OperationJSONContains: - return "@>" - case repository.OperationNotIn: - return "<>" - } - return "" -} - -var ( - placeholder = regexp.MustCompile(`\?`) -) - -// placeholder replaces all "?" with postgres placeholders ($) -func (db *CRDB) placeholder(query string) string { - occurances := placeholder.FindAllStringIndex(query, -1) - if len(occurances) == 0 { - return query - } - replaced := query[:occurances[0][0]] - - for i, l := range occurances { - nextIDX := len(query) - if i < len(occurances)-1 { - nextIDX = occurances[i+1][0] - } - replaced = replaced + "$" + strconv.Itoa(i+1) + query[l[1]:nextIDX] - } - return replaced -} - -func (db *CRDB) isUniqueViolationError(err error) bool { - if pgxErr, ok := err.(*pgconn.PgError); ok { - if pgxErr.Code == "23505" { - return true - } - } - return false -} diff --git a/internal/eventstore/repository/sql/local_crdb_test.go b/internal/eventstore/repository/sql/local_postgres_test.go similarity index 54% rename from internal/eventstore/repository/sql/local_crdb_test.go rename to internal/eventstore/repository/sql/local_postgres_test.go index 0f8c934b47..765da213e3 100644 --- a/internal/eventstore/repository/sql/local_crdb_test.go +++ b/internal/eventstore/repository/sql/local_postgres_test.go @@ -7,72 +7,61 @@ import ( "testing" "time" - "github.com/cockroachdb/cockroach-go/v2/testserver" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/initialise" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/cockroach" + "github.com/zitadel/zitadel/internal/database/dialect" + "github.com/zitadel/zitadel/internal/database/postgres" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" ) var ( - testCRDBClient *sql.DB + testClient *sql.DB ) func TestMain(m *testing.M) { - opts := make([]testserver.TestServerOpt, 0, 1) - if version := os.Getenv("ZITADEL_CRDB_VERSION"); version != "" { - opts = append(opts, testserver.CustomVersionOpt(version)) - } - ts, err := testserver.NewTestServer(opts...) - if err != nil { - logging.WithFields("error", err).Fatal("unable to start db") - } + os.Exit(func() int { + config, cleanup := postgres.StartEmbedded() + defer cleanup() - connConfig, err := pgxpool.ParseConfig(ts.PGURL().String()) - if err != nil { - logging.WithFields("error", err).Fatal("unable to parse db url") - } - connConfig.AfterConnect = new_es.RegisterEventstoreTypes - pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) - if err != nil { - logging.WithFields("error", err).Fatal("unable to create db pool") - } + connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) + logging.OnError(err).Fatal("unable to parse db url") - testCRDBClient = stdlib.OpenDBFromPool(pool) + connConfig.AfterConnect = new_es.RegisterEventstoreTypes + pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) + logging.OnError(err).Fatal("unable to create db pool") - if err = testCRDBClient.Ping(); err != nil { - logging.WithFields("error", err).Fatal("unable to ping db") - } + testClient = stdlib.OpenDBFromPool(pool) - defer func() { - testCRDBClient.Close() - ts.Stop() - }() + err = testClient.Ping() + logging.OnError(err).Fatal("unable to ping db") - if err = initDB(context.Background(), &database.DB{DB: testCRDBClient, Database: &cockroach.Config{Database: "zitadel"}}); err != nil { - logging.WithFields("error", err).Fatal("migrations failed") - } + defer func() { + logging.OnError(testClient.Close()).Error("unable to close db") + }() - os.Exit(m.Run()) + err = initDB(context.Background(), &database.DB{DB: testClient, Database: &postgres.Config{Database: "zitadel"}}) + logging.OnError(err).Fatal("migrations failed") + + return m.Run() + }()) } func initDB(ctx context.Context, db *database.DB) error { config := new(database.Config) - config.SetConnector(&cockroach.Config{User: cockroach.User{Username: "zitadel"}, Database: "zitadel"}) + config.SetConnector(&postgres.Config{User: postgres.User{Username: "zitadel"}, Database: "zitadel"}) - if err := initialise.ReadStmts("cockroach"); err != nil { + if err := initialise.ReadStmts(); err != nil { return err } err := initialise.Init(ctx, db, initialise.VerifyUser(config.Username(), ""), initialise.VerifyDatabase(config.DatabaseName()), - initialise.VerifyGrant(config.DatabaseName(), config.Username()), - initialise.VerifySettings(config.DatabaseName(), config.Username())) + initialise.VerifyGrant(config.DatabaseName(), config.Username())) if err != nil { return err } @@ -95,7 +84,7 @@ func (*testDB) DatabaseName() string { return "db" } func (*testDB) Username() string { return "user" } -func (*testDB) Type() string { return "cockroach" } +func (*testDB) Type() dialect.DatabaseType { return dialect.DatabaseTypePostgres } const oldEventsTable = `CREATE TABLE IF NOT EXISTS eventstore.events ( id UUID DEFAULT gen_random_uuid() @@ -116,5 +105,5 @@ const oldEventsTable = `CREATE TABLE IF NOT EXISTS eventstore.events ( , "position" DECIMAL NOT NULL , in_tx_order INTEGER NOT NULL - , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, event_sequence DESC) + , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, event_sequence) );` diff --git a/internal/eventstore/repository/sql/postgres.go b/internal/eventstore/repository/sql/postgres.go new file mode 100644 index 0000000000..bc9ad2e029 --- /dev/null +++ b/internal/eventstore/repository/sql/postgres.go @@ -0,0 +1,240 @@ +package sql + +import ( + "context" + "database/sql" + "errors" + "regexp" + "strconv" + + "github.com/jackc/pgx/v5/pgconn" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +// awaitOpenTransactions ensures event ordering, so we don't events younger that open transactions +var ( + awaitOpenTransactionsV1 = ` AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` + awaitOpenTransactionsV2 = ` AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` +) + +func awaitOpenTransactions(useV1 bool) string { + if useV1 { + return awaitOpenTransactionsV1 + } + return awaitOpenTransactionsV2 +} + +type Postgres struct { + *database.DB +} + +func NewPostgres(client *database.DB) *Postgres { + return &Postgres{client} +} + +func (db *Postgres) Health(ctx context.Context) error { return db.Ping() } + +// FilterToReducer finds all events matching the given search query and passes them to the reduce function. +func (psql *Postgres) FilterToReducer(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder, reduce eventstore.Reducer) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = query(ctx, psql, searchQuery, reduce, false) + if err == nil { + return nil + } + pgErr := new(pgconn.PgError) + // check events2 not exists + if errors.As(err, &pgErr) && pgErr.Code == "42P01" { + return query(ctx, psql, searchQuery, reduce, true) + } + return err +} + +// LatestSequence returns the latest sequence found by the search query +func (db *Postgres) LatestSequence(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (float64, error) { + var position sql.NullFloat64 + err := query(ctx, db, searchQuery, &position, false) + return position.Float64, err +} + +// InstanceIDs returns the instance ids found by the search query +func (db *Postgres) InstanceIDs(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) ([]string, error) { + var ids []string + err := query(ctx, db, searchQuery, &ids, false) + if err != nil { + return nil, err + } + return ids, nil +} + +func (db *Postgres) Client() *database.DB { + return db.DB +} + +func (db *Postgres) orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string { + if useV1 { + if desc { + return ` ORDER BY event_sequence DESC` + } + return ` ORDER BY event_sequence` + } + if shouldOrderBySequence { + if desc { + return ` ORDER BY "sequence" DESC` + } + return ` ORDER BY "sequence"` + } + + if desc { + return ` ORDER BY "position" DESC, in_tx_order DESC` + } + return ` ORDER BY "position", in_tx_order` +} + +func (db *Postgres) eventQuery(useV1 bool) string { + if useV1 { + return "SELECT" + + " creation_date" + + ", event_type" + + ", event_sequence" + + ", event_data" + + ", editor_user" + + ", resource_owner" + + ", instance_id" + + ", aggregate_type" + + ", aggregate_id" + + ", aggregate_version" + + " FROM eventstore.events" + } + return "SELECT" + + " created_at" + + ", event_type" + + `, "sequence"` + + `, "position"` + + ", payload" + + ", creator" + + `, "owner"` + + ", instance_id" + + ", aggregate_type" + + ", aggregate_id" + + ", revision" + + " FROM eventstore.events2" +} + +func (db *Postgres) maxSequenceQuery(useV1 bool) string { + if useV1 { + return `SELECT event_sequence FROM eventstore.events` + } + return `SELECT "position" FROM eventstore.events2` +} + +func (db *Postgres) instanceIDsQuery(useV1 bool) string { + table := "eventstore.events2" + if useV1 { + table = "eventstore.events" + } + return "SELECT DISTINCT instance_id FROM " + table +} + +func (db *Postgres) columnName(col repository.Field, useV1 bool) string { + switch col { + case repository.FieldAggregateID: + return "aggregate_id" + case repository.FieldAggregateType: + return "aggregate_type" + case repository.FieldSequence: + if useV1 { + return "event_sequence" + } + return `"sequence"` + case repository.FieldResourceOwner: + if useV1 { + return "resource_owner" + } + return `"owner"` + case repository.FieldInstanceID: + return "instance_id" + case repository.FieldEditorService: + if useV1 { + return "editor_service" + } + return "" + case repository.FieldEditorUser: + if useV1 { + return "editor_user" + } + return "creator" + case repository.FieldEventType: + return "event_type" + case repository.FieldEventData: + if useV1 { + return "event_data" + } + return "payload" + case repository.FieldCreationDate: + if useV1 { + return "creation_date" + } + return "created_at" + case repository.FieldPosition: + return `"position"` + default: + return "" + } +} + +func (db *Postgres) conditionFormat(operation repository.Operation) string { + switch operation { + case repository.OperationIn: + return "%s %s ANY(?)" + case repository.OperationNotIn: + return "%s %s ALL(?)" + case repository.OperationEquals, repository.OperationGreater, repository.OperationLess, repository.OperationJSONContains: + fallthrough + default: + return "%s %s ?" + } +} + +func (db *Postgres) operation(operation repository.Operation) string { + switch operation { + case repository.OperationEquals, repository.OperationIn: + return "=" + case repository.OperationGreater: + return ">" + case repository.OperationLess: + return "<" + case repository.OperationJSONContains: + return "@>" + case repository.OperationNotIn: + return "<>" + } + return "" +} + +var ( + placeholder = regexp.MustCompile(`\?`) +) + +// placeholder replaces all "?" with postgres placeholders ($) +func (db *Postgres) placeholder(query string) string { + occurrences := placeholder.FindAllStringIndex(query, -1) + if len(occurrences) == 0 { + return query + } + replaced := query[:occurrences[0][0]] + + for i, l := range occurrences { + nextIDX := len(query) + if i < len(occurrences)-1 { + nextIDX = occurrences[i+1][0] + } + replaced = replaced + "$" + strconv.Itoa(i+1) + query[l[1]:nextIDX] + } + return replaced +} diff --git a/internal/eventstore/repository/sql/crdb_test.go b/internal/eventstore/repository/sql/postgres_test.go similarity index 90% rename from internal/eventstore/repository/sql/crdb_test.go rename to internal/eventstore/repository/sql/postgres_test.go index a3f3331a82..151fdd1b6a 100644 --- a/internal/eventstore/repository/sql/crdb_test.go +++ b/internal/eventstore/repository/sql/postgres_test.go @@ -8,7 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/repository" ) -func TestCRDB_placeholder(t *testing.T) { +func TestPostgres_placeholder(t *testing.T) { type args struct { query string } @@ -50,15 +50,15 @@ func TestCRDB_placeholder(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if query := db.placeholder(tt.args.query); query != tt.res.query { - t.Errorf("CRDB.placeholder() = %v, want %v", query, tt.res.query) + t.Errorf("Postgres.placeholder() = %v, want %v", query, tt.res.query) } }) } } -func TestCRDB_operation(t *testing.T) { +func TestPostgres_operation(t *testing.T) { type res struct { op string } @@ -118,15 +118,15 @@ func TestCRDB_operation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if got := db.operation(tt.args.operation); got != tt.res.op { - t.Errorf("CRDB.operation() = %v, want %v", got, tt.res.op) + t.Errorf("Postgres.operation() = %v, want %v", got, tt.res.op) } }) } } -func TestCRDB_conditionFormat(t *testing.T) { +func TestPostgres_conditionFormat(t *testing.T) { type res struct { format string } @@ -159,15 +159,15 @@ func TestCRDB_conditionFormat(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if got := db.conditionFormat(tt.args.operation); got != tt.res.format { - t.Errorf("CRDB.conditionFormat() = %v, want %v", got, tt.res.format) + t.Errorf("Postgres.conditionFormat() = %v, want %v", got, tt.res.format) } }) } } -func TestCRDB_columnName(t *testing.T) { +func TestPostgres_columnName(t *testing.T) { type res struct { name string } @@ -295,9 +295,9 @@ func TestCRDB_columnName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if got := db.columnName(tt.args.field, tt.args.useV1); got != tt.res.name { - t.Errorf("CRDB.operation() = %v, want %v", got, tt.res.name) + t.Errorf("Postgres.operation() = %v, want %v", got, tt.res.name) } }) } diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index 4e1cc87aff..a545225d9e 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/eventstore" @@ -65,11 +64,6 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search if where == "" || query == "" { return zerrors.ThrowInvalidArgument(nil, "SQL-rWeBw", "invalid query factory") } - if q.Tx == nil { - if travel := prepareTimeTravel(ctx, criteria, q.AllowTimeTravel); travel != "" { - query += travel - } - } query += where // instead of using the max function of the database (which doesn't work for postgres) @@ -158,15 +152,7 @@ func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (s } } -func prepareTimeTravel(ctx context.Context, criteria querier, allow bool) string { - if !allow { - return "" - } - took := call.Took(ctx) - return criteria.Timetravel(took) -} - -func maxSequenceScanner(row scan, dest interface{}) (err error) { +func maxSequenceScanner(row scan, dest any) (err error) { position, ok := dest.(*sql.NullFloat64) if !ok { return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be sql.NullInt64 got: %T", dest) diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index abac19ead0..3df819be64 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -14,10 +14,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/cockroach" db_mock "github.com/zitadel/zitadel/internal/database/mock" + "github.com/zitadel/zitadel/internal/database/postgres" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" + new_es "github.com/zitadel/zitadel/internal/eventstore/v3" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -68,7 +69,7 @@ func Test_getCondition(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{} + db := &Postgres{} if got := getCondition(db, tt.args.filter, false); got != tt.want { t.Errorf("getCondition() = %v, want %v", got, tt.want) } @@ -236,8 +237,7 @@ func Test_prepareColumns(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - crdb := &CRDB{} - query, rowScanner := prepareColumns(crdb, tt.args.columns, tt.args.useV1) + query, rowScanner := prepareColumns(new(Postgres), tt.args.columns, tt.args.useV1) if query != tt.res.query { t.Errorf("prepareColumns() got = %s, want %s", query, tt.res.query) } @@ -267,7 +267,7 @@ func Test_prepareColumns(t *testing.T) { got := reflect.Indirect(reflect.ValueOf(tt.args.dest)).Interface() if !reflect.DeepEqual(got, tt.res.expected) { - t.Errorf("unexpected result from rowScanner \nwant: %+v \ngot: %+v", tt.res.expected, got) + t.Errorf("unexpected result from rowScanner nwant: %+v ngot: %+v", tt.res.expected, got) } }) } @@ -403,7 +403,7 @@ func Test_prepareCondition(t *testing.T) { useV1: true, }, res: res{ - clause: " WHERE aggregate_type = ANY(?) AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))", + clause: " WHERE aggregate_type = ANY(?) AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')", values: []interface{}{[]eventstore.AggregateType{"user", "org"}, database.TextArray[string]{}}, }, }, @@ -420,7 +420,7 @@ func Test_prepareCondition(t *testing.T) { }, }, res: res{ - clause: ` WHERE aggregate_type = ANY(?) AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))`, + clause: ` WHERE aggregate_type = ANY(?) AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')`, values: []interface{}{[]eventstore.AggregateType{"user", "org"}, database.TextArray[string]{}}, }, }, @@ -440,7 +440,7 @@ func Test_prepareCondition(t *testing.T) { useV1: true, }, res: res{ - clause: " WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))", + clause: " WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')", values: []interface{}{[]eventstore.AggregateType{"user", "org"}, "1234", []eventstore.EventType{"user.created", "org.created"}, database.TextArray[string]{}}, }, }, @@ -459,15 +459,14 @@ func Test_prepareCondition(t *testing.T) { }, }, res: res{ - clause: ` WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))`, + clause: ` WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')`, values: []interface{}{[]eventstore.AggregateType{"user", "org"}, "1234", []eventstore.EventType{"user.created", "org.created"}, database.TextArray[string]{}}, }, }, } - crdb := NewCRDB(&database.DB{Database: new(cockroach.Config)}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotClause, gotValues := prepareConditions(crdb, tt.args.query, tt.args.useV1) + gotClause, gotValues := prepareConditions(NewPostgres(&database.DB{Database: new(postgres.Config)}), tt.args.query, tt.args.useV1) if gotClause != tt.res.clause { t.Errorf("prepareCondition() gotClause = %v, want %v", gotClause, tt.res.clause) } @@ -484,7 +483,7 @@ func Test_prepareCondition(t *testing.T) { } } -func Test_query_events_with_crdb(t *testing.T) { +func Test_query_events_with_postgres(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -511,7 +510,7 @@ func Test_query_events_with_crdb(t *testing.T) { Builder(), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "300"), generateEvent(t, "300"), @@ -532,7 +531,7 @@ func Test_query_events_with_crdb(t *testing.T) { Builder(), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "301"), generateEvent(t, "302"), @@ -555,7 +554,7 @@ func Test_query_events_with_crdb(t *testing.T) { Builder(), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "303"), generateEvent(t, "303"), @@ -576,7 +575,7 @@ func Test_query_events_with_crdb(t *testing.T) { ResourceOwner("caos"), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "306", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), generateEvent(t, "307", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), @@ -599,7 +598,7 @@ func Test_query_events_with_crdb(t *testing.T) { Builder(), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{ generateEvent(t, "311", func(e *repository.Event) { e.Typ = "user.created" }), generateEvent(t, "311", func(e *repository.Event) { e.Typ = "user.updated" }), @@ -623,7 +622,7 @@ func Test_query_events_with_crdb(t *testing.T) { searchQuery: eventstore.NewSearchQueryBuilder(eventstore.Columns(-1)), }, fields: fields{ - client: testCRDBClient, + client: testClient, existingEvents: []eventstore.Command{}, }, res: res{ @@ -634,117 +633,37 @@ func Test_query_events_with_crdb(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := &CRDB{ - DB: &database.DB{ - DB: tt.fields.client, - Database: new(testDB), - }, + dbClient := &database.DB{ + DB: tt.fields.client, + Database: new(testDB), } + client := &Postgres{ + DB: dbClient, + } + + pusher := new_es.NewEventstore(dbClient) // setup initial data for query - if _, err := db.Push(context.Background(), tt.fields.existingEvents...); err != nil { + if _, err := pusher.Push(context.Background(), dbClient.DB, tt.fields.existingEvents...); err != nil { t.Errorf("error in setup = %v", err) return } events := []eventstore.Event{} - if err := query(context.Background(), db, tt.args.searchQuery, eventstore.Reducer(func(event eventstore.Event) error { + if err := query(context.Background(), client, tt.args.searchQuery, eventstore.Reducer(func(event eventstore.Event) error { events = append(events, event) return nil }), true); (err != nil) != tt.wantErr { - t.Errorf("CRDB.query() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } }) } } -/* Cockroach test DB doesn't seem to lock -func Test_query_events_with_crdb_locking(t *testing.T) { - type args struct { - searchQuery *eventstore.SearchQueryBuilder - } - type fields struct { - existingEvents []eventstore.Command - client *sql.DB - } - tests := []struct { - name string - fields fields - args args - lockOption eventstore.LockOption - wantErr bool - }{ - { - name: "skip locked", - fields: fields{ - client: testCRDBClient, - existingEvents: []eventstore.Command{ - generateEvent(t, "306", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), - generateEvent(t, "307", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), - generateEvent(t, "308", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), - }, - }, - args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - ResourceOwner("caos"), - }, - lockOption: eventstore.LockOptionNoWait, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := &CRDB{ - DB: &database.DB{ - DB: tt.fields.client, - Database: new(testDB), - }, - } - // setup initial data for query - if _, err := db.Push(context.Background(), tt.fields.existingEvents...); err != nil { - t.Errorf("error in setup = %v", err) - return - } - // first TX should lock and return all events - tx1, err := db.DB.Begin() - require.NoError(t, err) - defer func() { - require.NoError(t, tx1.Rollback()) - }() - searchQuery1 := tt.args.searchQuery.LockRowsDuringTx(tx1, tt.lockOption) - gotEvents1 := []eventstore.Event{} - err = query(context.Background(), db, searchQuery1, eventstore.Reducer(func(event eventstore.Event) error { - gotEvents1 = append(gotEvents1, event) - return nil - }), true) - require.NoError(t, err) - assert.Len(t, gotEvents1, len(tt.fields.existingEvents)) - - // second TX should not return the events, and might return an error - tx2, err := db.DB.Begin() - require.NoError(t, err) - defer func() { - require.NoError(t, tx2.Rollback()) - }() - searchQuery2 := tt.args.searchQuery.LockRowsDuringTx(tx1, tt.lockOption) - gotEvents2 := []eventstore.Event{} - err = query(context.Background(), db, searchQuery2, eventstore.Reducer(func(event eventstore.Event) error { - gotEvents2 = append(gotEvents2, event) - return nil - }), true) - if tt.wantErr { - require.Error(t, err) - } - require.NoError(t, err) - assert.Len(t, gotEvents2, 0) - }) - } -} -*/ - func Test_query_events_mocked(t *testing.T) { type args struct { query *eventstore.SearchQueryBuilder - dest interface{} + dest any useV1 bool } type res struct { @@ -772,8 +691,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence DESC`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, ), }, @@ -795,8 +714,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence LIMIT \$3`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence LIMIT $3`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, ), }, @@ -818,32 +737,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC LIMIT \$3`, - []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, - ), - }, - res: res{ - wantErr: false, - }, - }, - { - name: "with limit and order by desc as of system time", - args: args{ - dest: &[]*repository.Event{}, - query: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - OrderDesc(). - AwaitOpenTransactions(). - Limit(5). - AllowTimeTravel(). - AddQuery(). - AggregateTypes("user"). - Builder(), - useV1: true, - }, - fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events AS OF SYSTEM TIME '-1 ms' WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC LIMIT \$3`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence DESC LIMIT $3`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, ), }, @@ -864,8 +759,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC LIMIT $2 FOR UPDATE`), []driver.Value{eventstore.AggregateType("user"), uint64(5)}, ), }, @@ -886,8 +781,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE NOWAIT`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC LIMIT $2 FOR UPDATE NOWAIT`), []driver.Value{eventstore.AggregateType("user"), uint64(5)}, ), }, @@ -908,8 +803,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE SKIP LOCKED`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC LIMIT $2 FOR UPDATE SKIP LOCKED`), []driver.Value{eventstore.AggregateType("user"), uint64(5)}, ), }, @@ -931,8 +826,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQueryErr(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + mock: newMockClient(t).expectQueryErr( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence DESC`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, sql.ErrConnDone), }, @@ -954,8 +849,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQueryScanErr(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + mock: newMockClient(t).expectQueryScanErr( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($2) AND state <> 'idle') ORDER BY event_sequence DESC`), []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, &repository.Event{Seq: 100}), }, @@ -989,8 +884,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE \(aggregate_type = \$1 OR \(aggregate_type = \$2 AND aggregate_id = \$3\)\) AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$4\)\) ORDER BY event_sequence DESC LIMIT \$5`, + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE (aggregate_type = $1 OR (aggregate_type = $2 AND aggregate_id = $3)) AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY($4) AND state <> 'idle') ORDER BY event_sequence DESC LIMIT $5`), []driver.Value{eventstore.AggregateType("user"), eventstore.AggregateType("org"), "asdf42", database.TextArray[string]{}, uint64(5)}, ), }, @@ -1018,10 +913,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: true, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - regexp.QuoteMeta( - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY event_sequence DESC LIMIT $9`, - ), + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY event_sequence DESC LIMIT $9`), []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, ), }, @@ -1049,10 +942,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: false, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - regexp.QuoteMeta( - `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`, - ), + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`), []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, ), }, @@ -1080,10 +971,8 @@ func Test_query_events_mocked(t *testing.T) { useV1: false, }, fields: fields{ - mock: newMockClient(t).expectQuery(t, - regexp.QuoteMeta( - `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND created_at > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND created_at > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`, - ), + mock: newMockClient(t).expectQuery( + regexp.QuoteMeta(`SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND created_at > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND created_at > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`), []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), time.Unix(123, 456), eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", time.Unix(123, 456), uint64(5)}, ), }, @@ -1092,14 +981,14 @@ func Test_query_events_mocked(t *testing.T) { }, }, } - crdb := NewCRDB(&database.DB{Database: new(testDB)}) + client := NewPostgres(&database.DB{Database: new(testDB)}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.fields.mock != nil { - crdb.DB.DB = tt.fields.mock.client + client.DB.DB = tt.fields.mock.client } - err := query(context.Background(), crdb, tt.args.query, tt.args.dest, tt.args.useV1) + err := query(context.Background(), client, tt.args.query, tt.args.dest, tt.args.useV1) if (err != nil) != tt.res.wantErr { t.Errorf("query() error = %v, wantErr %v", err, tt.res.wantErr) } @@ -1120,7 +1009,7 @@ type dbMock struct { client *sql.DB } -func (m *dbMock) expectQuery(t *testing.T, expectedQuery string, args []driver.Value, events ...*repository.Event) *dbMock { +func (m *dbMock) expectQuery(expectedQuery string, args []driver.Value, events ...*repository.Event) *dbMock { query := m.mock.ExpectQuery(expectedQuery).WithArgs(args...) rows := m.mock.NewRows([]string{"sequence"}) for _, event := range events { @@ -1130,7 +1019,7 @@ func (m *dbMock) expectQuery(t *testing.T, expectedQuery string, args []driver.V return m } -func (m *dbMock) expectQueryScanErr(t *testing.T, expectedQuery string, args []driver.Value, events ...*repository.Event) *dbMock { +func (m *dbMock) expectQueryScanErr(expectedQuery string, args []driver.Value, events ...*repository.Event) *dbMock { query := m.mock.ExpectQuery(expectedQuery).WithArgs(args...) rows := m.mock.NewRows([]string{"sequence"}) for _, event := range events { @@ -1140,7 +1029,7 @@ func (m *dbMock) expectQueryScanErr(t *testing.T, expectedQuery string, args []d return m } -func (m *dbMock) expectQueryErr(t *testing.T, expectedQuery string, args []driver.Value, err error) *dbMock { +func (m *dbMock) expectQueryErr(expectedQuery string, args []driver.Value, err error) *dbMock { m.mock.ExpectQuery(expectedQuery).WithArgs(args...).WillReturnError(err) return m } diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index df38d15def..1596936a36 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -25,7 +25,6 @@ type SearchQueryBuilder struct { tx *sql.Tx lockRows bool lockOption LockOption - allowTimeTravel bool positionAfter float64 awaitOpenTransactions bool creationDateAfter time.Time @@ -77,10 +76,6 @@ func (b *SearchQueryBuilder) GetTx() *sql.Tx { return b.tx } -func (b *SearchQueryBuilder) GetAllowTimeTravel() bool { - return b.allowTimeTravel -} - func (b SearchQueryBuilder) GetPositionAfter() float64 { return b.positionAfter } @@ -289,13 +284,6 @@ func (builder *SearchQueryBuilder) EditorUser(id string) *SearchQueryBuilder { return builder } -// AllowTimeTravel activates the time travel feature of the database if supported -// The queries will be made based on the call time -func (builder *SearchQueryBuilder) AllowTimeTravel() *SearchQueryBuilder { - builder.allowTimeTravel = true - return builder -} - // PositionAfter filters for events which happened after the specified time func (builder *SearchQueryBuilder) PositionAfter(position float64) *SearchQueryBuilder { builder.positionAfter = position diff --git a/internal/eventstore/search_query_test.go b/internal/eventstore/search_query_test.go index 8c654911ea..b8f570dc0d 100644 --- a/internal/eventstore/search_query_test.go +++ b/internal/eventstore/search_query_test.go @@ -45,16 +45,6 @@ func testSetLimit(limit uint64) func(builder *SearchQueryBuilder) *SearchQueryBu } } -func testOr(queryFuncs ...func(*SearchQuery) *SearchQuery) func(*SearchQuery) *SearchQuery { - return func(query *SearchQuery) *SearchQuery { - subQuery := query.Or() - for _, queryFunc := range queryFuncs { - queryFunc(subQuery) - } - return subQuery - } -} - func testSetAggregateTypes(types ...AggregateType) func(*SearchQuery) *SearchQuery { return func(query *SearchQuery) *SearchQuery { query = query.AggregateTypes(types...) diff --git a/internal/eventstore/subscription.go b/internal/eventstore/subscription.go index c76c81df19..076d16ad52 100644 --- a/internal/eventstore/subscription.go +++ b/internal/eventstore/subscription.go @@ -1,6 +1,7 @@ package eventstore import ( + "slices" "sync" "github.com/zitadel/logging" @@ -8,7 +9,7 @@ import ( var ( subscriptions = map[AggregateType][]*Subscription{} - subsMutext sync.Mutex + subsMutex sync.RWMutex ) type Subscription struct { @@ -27,8 +28,8 @@ func SubscribeAggregates(eventQueue chan Event, aggregates ...AggregateType) *Su types: types, } - subsMutext.Lock() - defer subsMutext.Unlock() + subsMutex.Lock() + defer subsMutex.Unlock() for _, aggregate := range aggregates { subscriptions[aggregate] = append(subscriptions[aggregate], sub) @@ -45,8 +46,8 @@ func SubscribeEventTypes(eventQueue chan Event, types map[AggregateType][]EventT types: types, } - subsMutext.Lock() - defer subsMutext.Unlock() + subsMutex.Lock() + defer subsMutex.Unlock() for aggregate := range types { subscriptions[aggregate] = append(subscriptions[aggregate], sub) @@ -56,8 +57,8 @@ func SubscribeEventTypes(eventQueue chan Event, types map[AggregateType][]EventT } func (es *Eventstore) notify(events []Event) { - subsMutext.Lock() - defer subsMutext.Unlock() + subsMutex.RLock() + defer subsMutex.RUnlock() for _, event := range events { subs, ok := subscriptions[event.Aggregate().Type] if !ok { @@ -71,14 +72,11 @@ func (es *Eventstore) notify(events []Event) { continue } //subscription for certain events - for _, eventType := range eventTypes { - if event.Type() == eventType { - select { - case sub.Events <- event: - default: - logging.Debug("unable to push event") - } - break + if slices.Contains(eventTypes, event.Type()) { + select { + case sub.Events <- event: + default: + logging.Debug("unable to push event") } } } @@ -86,8 +84,8 @@ func (es *Eventstore) notify(events []Event) { } func (s *Subscription) Unsubscribe() { - subsMutext.Lock() - defer subsMutext.Unlock() + subsMutex.Lock() + defer subsMutex.Unlock() for aggregate := range s.types { subs, ok := subscriptions[aggregate] if !ok { diff --git a/internal/eventstore/v3/eventstore.go b/internal/eventstore/v3/eventstore.go index 1bb515527c..424805c882 100644 --- a/internal/eventstore/v3/eventstore.go +++ b/internal/eventstore/v3/eventstore.go @@ -24,9 +24,9 @@ func init() { var ( // pushPlaceholderFmt defines how data are inserted into the events table - pushPlaceholderFmt string + pushPlaceholderFmt = "($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $%d)" // uniqueConstraintPlaceholderFmt defines the format of the unique constraint error returned from the database - uniqueConstraintPlaceholderFmt string + uniqueConstraintPlaceholderFmt = "(%s, %s, %s)" _ eventstore.Pusher = (*Eventstore)(nil) ) @@ -158,15 +158,6 @@ func (es *Eventstore) Client() *database.DB { } func NewEventstore(client *database.DB) *Eventstore { - switch client.Type() { - case "cockroach": - pushPlaceholderFmt = "($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $%d)" - uniqueConstraintPlaceholderFmt = "('%s', '%s', '%s')" - case "postgres": - pushPlaceholderFmt = "($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $%d)" - uniqueConstraintPlaceholderFmt = "(%s, %s, %s)" - } - return &Eventstore{client: client} } @@ -200,14 +191,8 @@ func (es *Eventstore) pushTx(ctx context.Context, client database.ContextQueryEx beginner = es.client } - isolationLevel := sql.LevelReadCommitted - // cockroach requires serializable to execute the push function - // because we use [cluster_logical_timestamp()](https://www.cockroachlabs.com/docs/stable/functions-and-operators#system-info-functions) - if es.client.Type() == "cockroach" { - isolationLevel = sql.LevelSerializable - } tx, err = beginner.BeginTx(ctx, &sql.TxOptions{ - Isolation: isolationLevel, + Isolation: sql.LevelReadCommitted, ReadOnly: false, }) if err != nil { diff --git a/internal/eventstore/v3/field.go b/internal/eventstore/v3/field.go index b399e7f5e8..e8f761d410 100644 --- a/internal/eventstore/v3/field.go +++ b/internal/eventstore/v3/field.go @@ -47,7 +47,7 @@ func (es *Eventstore) FillFields(ctx context.Context, events ...eventstore.FillF // Search implements the [eventstore.Search] method func (es *Eventstore) Search(ctx context.Context, conditions ...map[eventstore.FieldType]any) (result []*eventstore.SearchResult, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() var builder strings.Builder args := buildSearchStatement(ctx, &builder, conditions...) @@ -156,10 +156,11 @@ func (es *Eventstore) handleFieldCommands(ctx context.Context, tx database.Tx, c func handleFieldFillEvents(ctx context.Context, tx database.Tx, events []eventstore.FillFieldsEvent) error { for _, event := range events { - if len(event.Fields()) > 0 { - if err := handleFieldOperations(ctx, tx, event.Fields()); err != nil { - return err - } + if len(event.Fields()) == 0 { + continue + } + if err := handleFieldOperations(ctx, tx, event.Fields()); err != nil { + return err } } return nil diff --git a/internal/eventstore/v3/push_test.go b/internal/eventstore/v3/push_test.go index a6c4f515fd..da583891e9 100644 --- a/internal/eventstore/v3/push_test.go +++ b/internal/eventstore/v3/push_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/cockroach" + "github.com/zitadel/zitadel/internal/database/postgres" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -65,7 +65,7 @@ func Test_mapCommands(t *testing.T) { ), }, placeHolders: []string{ - "($1, $2, $3, $4, $5, $6, $7, $8, $9, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $10)", + "($1, $2, $3, $4, $5, $6, $7, $8, $9, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $10)", }, args: []any{ "instance", @@ -114,8 +114,8 @@ func Test_mapCommands(t *testing.T) { ), }, placeHolders: []string{ - "($1, $2, $3, $4, $5, $6, $7, $8, $9, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $10)", - "($11, $12, $13, $14, $15, $16, $17, $18, $19, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $20)", + "($1, $2, $3, $4, $5, $6, $7, $8, $9, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $10)", + "($11, $12, $13, $14, $15, $16, $17, $18, $19, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $20)", }, args: []any{ // first event @@ -180,8 +180,8 @@ func Test_mapCommands(t *testing.T) { ), }, placeHolders: []string{ - "($1, $2, $3, $4, $5, $6, $7, $8, $9, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $10)", - "($11, $12, $13, $14, $15, $16, $17, $18, $19, hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp(), $20)", + "($1, $2, $3, $4, $5, $6, $7, $8, $9, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $10)", + "($11, $12, $13, $14, $15, $16, $17, $18, $19, statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()), $20)", }, args: []any{ // first event @@ -236,7 +236,7 @@ func Test_mapCommands(t *testing.T) { } } // is used to set the the [pushPlaceholderFmt] - NewEventstore(&database.DB{Database: new(cockroach.Config)}) + NewEventstore(&database.DB{Database: new(postgres.Config)}) t.Run(tt.name, func(t *testing.T) { defer func() { cause := recover() diff --git a/internal/eventstore/v3/push_without_func.go b/internal/eventstore/v3/push_without_func.go index 914b880204..b94a9e8f54 100644 --- a/internal/eventstore/v3/push_without_func.go +++ b/internal/eventstore/v3/push_without_func.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/cockroachdb/cockroach-go/v2/crdb" "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" @@ -16,25 +15,6 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -type transaction struct { - database.Tx -} - -var _ crdb.Tx = (*transaction)(nil) - -func (t *transaction) Exec(ctx context.Context, query string, args ...interface{}) error { - _, err := t.Tx.ExecContext(ctx, query, args...) - return err -} - -func (t *transaction) Commit(ctx context.Context) error { - return t.Tx.Commit() -} - -func (t *transaction) Rollback(ctx context.Context) error { - return t.Tx.Rollback() -} - // checks whether the error is caused because setup step 39 was not executed func isSetupNotExecutedError(err error) bool { if err == nil { @@ -64,7 +44,6 @@ func (es *Eventstore) pushWithoutFunc(ctx context.Context, client database.Conte err = closeTx(err) }() - // tx is not closed because [crdb.ExecuteInTx] takes care of that var ( sequences []*latestSequence ) diff --git a/internal/execution/ctx.go b/internal/execution/ctx.go new file mode 100644 index 0000000000..9e6bac3e30 --- /dev/null +++ b/internal/execution/ctx.go @@ -0,0 +1,19 @@ +package execution + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ExecutionUserID = "EXECUTION" + +func HandlerContext(event *eventstore.Aggregate) context.Context { + ctx := authz.WithInstanceID(context.Background(), event.InstanceID) + return authz.SetCtxData(ctx, authz.CtxData{UserID: ExecutionUserID, OrgID: event.ResourceOwner}) +} + +func ContextWithExecuter(ctx context.Context, aggregate *eventstore.Aggregate) context.Context { + return authz.SetCtxData(ctx, authz.CtxData{UserID: ExecutionUserID, OrgID: aggregate.ResourceOwner}) +} diff --git a/internal/execution/execution.go b/internal/execution/execution.go index 99d7f6182f..b885858d94 100644 --- a/internal/execution/execution.go +++ b/internal/execution/execution.go @@ -6,12 +6,15 @@ import ( "encoding/json" "io" "net/http" + "strings" "time" "github.com/zitadel/logging" zhttp "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/actions" @@ -39,7 +42,7 @@ func CallTargets( info ContextInfo, ) (_ interface{}, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() for _, target := range targets { // call the type of target @@ -69,7 +72,7 @@ func CallTarget( info ContextInfoRequest, ) (res []byte, err error) { ctx, span := tracing.NewSpan(ctx) - defer span.EndWithError(err) + defer func() { span.EndWithError(err) }() switch target.GetTargetType() { // get request, ignore response and return request and error for handling in list of targets @@ -79,11 +82,11 @@ func CallTarget( case domain.TargetTypeCall: return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey()) case domain.TargetTypeAsync: - go func(target Target, info ContextInfoRequest) { - if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey()); err != nil { + go func(ctx context.Context, target Target, info []byte) { + if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info, target.GetSigningKey()); err != nil { logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err) } - }(target, info) + }(context.WithoutCancel(ctx), target, info.GetHTTPRequestBody()) return nil, nil default: return nil, zerrors.ThrowInternal(nil, "EXEC-auqnansr2m", "Errors.Execution.Unknown") @@ -153,3 +156,59 @@ type ErrorBody struct { ForwardedStatusCode int `json:"forwardedStatusCode,omitempty"` ForwardedErrorMessage string `json:"forwardedErrorMessage,omitempty"` } + +type ExecutionTargetsQueries interface { + TargetsByExecutionID(ctx context.Context, ids []string) (execution []*query.ExecutionTarget, err error) + TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*query.ExecutionTarget, err error) +} + +func QueryExecutionTargetsForRequestAndResponse( + ctx context.Context, + queries ExecutionTargetsQueries, + fullMethod string, +) ([]Target, []Target) { + ctx, span := tracing.NewSpan(ctx) + defer span.End() + + targets, err := queries.TargetsByExecutionIDs(ctx, + idsForFullMethod(fullMethod, domain.ExecutionTypeRequest), + idsForFullMethod(fullMethod, domain.ExecutionTypeResponse), + ) + requestTargets := make([]Target, 0, len(targets)) + responseTargets := make([]Target, 0, len(targets)) + if err != nil { + logging.WithFields("fullMethod", fullMethod).WithError(err).Info("unable to query targets") + return requestTargets, responseTargets + } + + for _, target := range targets { + if strings.HasPrefix(target.GetExecutionID(), execution.IDAll(domain.ExecutionTypeRequest)) { + requestTargets = append(requestTargets, target) + } else if strings.HasPrefix(target.GetExecutionID(), execution.IDAll(domain.ExecutionTypeResponse)) { + responseTargets = append(responseTargets, target) + } + } + + return requestTargets, responseTargets +} + +func idsForFullMethod(fullMethod string, executionType domain.ExecutionType) []string { + return []string{execution.ID(executionType, fullMethod), execution.ID(executionType, serviceFromFullMethod(fullMethod)), execution.IDAll(executionType)} +} + +func serviceFromFullMethod(s string) string { + parts := strings.Split(s, "/") + return parts[1] +} + +func QueryExecutionTargetsForFunction(ctx context.Context, query ExecutionTargetsQueries, function string) ([]Target, error) { + queriedActionsV2, err := query.TargetsByExecutionID(ctx, []string{function}) + if err != nil { + return nil, err + } + executionTargets := make([]Target, len(queriedActionsV2)) + for i, action := range queriedActionsV2 { + executionTargets[i] = action + } + return executionTargets, nil +} diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go index 5a45d96625..036b160ab7 100644 --- a/internal/execution/execution_test.go +++ b/internal/execution/execution_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" "github.com/zitadel/zitadel/internal/domain" @@ -61,7 +62,7 @@ func Test_Call(t *testing.T) { args{ ctx: context.Background(), timeout: time.Second, - sleep: time.Second, + sleep: 2 * time.Second, method: http.MethodPost, body: []byte("{\"request\": \"values\"}"), respBody: []byte("{\"response\": \"values\"}"), @@ -149,8 +150,8 @@ func Test_CallTarget(t *testing.T) { info: requestContextInfo1, server: &callTestServer{ method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), timeout: time.Second, statusCode: http.StatusInternalServerError, }, @@ -170,8 +171,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusInternalServerError, }, target: &mockTarget{ @@ -191,8 +192,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, }, target: &mockTarget{ @@ -212,8 +213,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, signingKey: "signingkey", }, @@ -235,8 +236,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusInternalServerError, }, target: &mockTarget{ @@ -256,8 +257,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, }, target: &mockTarget{ @@ -266,7 +267,7 @@ func Test_CallTarget(t *testing.T) { }, }, res{ - body: []byte("{\"request\":\"content2\"}"), + body: []byte("{\"content\":\"request2\"}"), }, }, { @@ -277,8 +278,8 @@ func Test_CallTarget(t *testing.T) { server: &callTestServer{ timeout: time.Second, method: http.MethodPost, - expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), - respondBody: []byte("{\"request\":\"content2\"}"), + expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"), + respondBody: []byte("{\"content\":\"request2\"}"), statusCode: http.StatusOK, signingKey: "signingkey", }, @@ -289,7 +290,7 @@ func Test_CallTarget(t *testing.T) { }, }, res{ - body: []byte("{\"request\":\"content2\"}"), + body: []byte("{\"content\":\"request2\"}"), }, }, } @@ -576,13 +577,13 @@ func testCallTargets(ctx context.Context, } var requestContextInfo1 = &middleware.ContextInfoRequest{ - Request: &request{ - Request: "content1", - }, + Request: middleware.Message{Message: &structpb.Struct{ + Fields: map[string]*structpb.Value{"content": structpb.NewStringValue("request1")}, + }}, } -var requestContextInfoBody1 = []byte("{\"request\":{\"request\":\"content1\"}}") -var requestContextInfoBody2 = []byte("{\"request\":{\"request\":\"content2\"}}") +var requestContextInfoBody1 = []byte("{\"request\":{\"content\":\"request1\"}}") +var requestContextInfoBody2 = []byte("{\"request\":{\"content\":\"request2\"}}") type request struct { Request string `json:"request"` diff --git a/internal/execution/gen_mock.go b/internal/execution/gen_mock.go new file mode 100644 index 0000000000..93eebfbb02 --- /dev/null +++ b/internal/execution/gen_mock.go @@ -0,0 +1,4 @@ +package execution + +//go:generate mockgen -package mock -destination ./mock/queries.mock.go github.com/zitadel/zitadel/internal/execution Queries +//go:generate mockgen -package mock -destination ./mock/queue.mock.go github.com/zitadel/zitadel/internal/execution Queue diff --git a/internal/execution/handlers.go b/internal/execution/handlers.go new file mode 100644 index 0000000000..7ffb4cc6ff --- /dev/null +++ b/internal/execution/handlers.go @@ -0,0 +1,156 @@ +package execution + +import ( + "context" + "encoding/json" + "slices" + "strings" + + "github.com/riverqueue/river" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/queue" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" +) + +const ( + HandlerTable = "projections.execution_handler" +) + +type Queue interface { + Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error +} + +type Queries interface { + TargetsByExecutionID(ctx context.Context, ids []string) (execution []*query.ExecutionTarget, err error) + InstanceByID(ctx context.Context, id string) (instance authz.Instance, err error) +} + +type eventHandler struct { + eventTypes []string + aggregateTypeFromEventType func(typ eventstore.EventType) eventstore.AggregateType + query Queries + queue Queue +} + +func NewEventHandler( + ctx context.Context, + config handler.Config, + eventTypes []string, + aggregateTypeFromEventType func(typ eventstore.EventType) eventstore.AggregateType, + query Queries, + queue Queue, +) *handler.Handler { + return handler.NewHandler(ctx, &config, &eventHandler{ + eventTypes: eventTypes, + aggregateTypeFromEventType: aggregateTypeFromEventType, + query: query, + queue: queue, + }) +} + +func (u *eventHandler) Name() string { + return HandlerTable +} + +func (u *eventHandler) Reducers() []handler.AggregateReducer { + aggList := make(map[eventstore.AggregateType][]eventstore.EventType) + for _, eventType := range u.eventTypes { + aggType := u.aggregateTypeFromEventType(eventstore.EventType(eventType)) + aggEventTypes := aggList[aggType] + if !slices.Contains(aggEventTypes, eventstore.EventType(eventType)) { + aggList[aggType] = append(aggList[aggType], eventstore.EventType(eventType)) + } + } + + aggReducers := make([]handler.AggregateReducer, 0, len(aggList)) + for aggType, aggEventTypes := range aggList { + eventReducers := make([]handler.EventReducer, len(aggEventTypes)) + for j, eventType := range aggEventTypes { + eventReducers[j] = handler.EventReducer{ + Event: eventType, + Reduce: u.reduce, + } + } + aggReducers = append(aggReducers, handler.AggregateReducer{ + Aggregate: aggType, + EventReducers: eventReducers, + }) + } + return aggReducers +} + +func groupsFromEventType(s string) []string { + parts := strings.Split(s, ".") + groups := make([]string, len(parts)) + for i := range parts { + groups[i] = strings.Join(parts[:i+1], ".") + if i < len(parts)-1 { + groups[i] += ".*" + } + } + slices.Reverse(groups) + return groups +} + +func idsForEventType(eventType string) []string { + ids := make([]string, 0) + for _, group := range groupsFromEventType(eventType) { + ids = append(ids, + exec_repo.ID(domain.ExecutionTypeEvent, group), + ) + } + return append(ids, + exec_repo.IDAll(domain.ExecutionTypeEvent), + ) +} + +func (u *eventHandler) reduce(e eventstore.Event) (*handler.Statement, error) { + ctx := HandlerContext(e.Aggregate()) + + targets, err := u.query.TargetsByExecutionID(ctx, idsForEventType(string(e.Type()))) + if err != nil { + return nil, err + } + + // no execution from worker necessary + if len(targets) == 0 { + return handler.NewNoOpStatement(e), nil + } + + return handler.NewStatement(e, func(ex handler.Executer, projectionName string) error { + ctx := HandlerContext(e.Aggregate()) + req, err := NewRequest(e, targets) + if err != nil { + return err + } + return u.queue.Insert(ctx, + req, + queue.WithQueueName(exec_repo.QueueName), + ) + }), nil +} + +func NewRequest(e eventstore.Event, targets []*query.ExecutionTarget) (*exec_repo.Request, error) { + targetsData, err := json.Marshal(targets) + if err != nil { + return nil, err + } + eventData, err := json.Marshal(e) + if err != nil { + return nil, err + } + return &exec_repo.Request{ + Aggregate: e.Aggregate(), + Sequence: e.Sequence(), + EventType: e.Type(), + CreatedAt: e.CreatedAt(), + UserID: e.Creator(), + EventData: eventData, + TargetsData: targetsData, + }, nil +} diff --git a/internal/execution/handlers_test.go b/internal/execution/handlers_test.go new file mode 100644 index 0000000000..de220abcc0 --- /dev/null +++ b/internal/execution/handlers_test.go @@ -0,0 +1,487 @@ +package execution + +import ( + "database/sql" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/execution/mock" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/action" + execution_rp "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/repository/session" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func Test_EventExecution(t *testing.T) { + type args struct { + event eventstore.Event + targets []*query.ExecutionTarget + } + type res struct { + targets []Target + contextInfo *execution_rp.ContextInfoEvent + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "session added, ok", + args{ + event: &eventstore.BaseEvent{ + Agg: &eventstore.Aggregate{ + ID: "aggID", + Type: session.AggregateType, + ResourceOwner: "resourceOwner", + InstanceID: "instanceID", + Version: session.AggregateVersion, + }, + EventType: session.AddedType, + Seq: 1, + Creation: time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC), + User: userID, + Data: []byte(`{"ID":"","Seq":1,"Pos":0,"Creation":"2024-01-01T01:01:01.000000001Z"}`), + }, + targets: []*query.ExecutionTarget{{ + InstanceID: instanceID, + ExecutionID: "executionID", + TargetID: "targetID", + TargetType: domain.TargetTypeWebhook, + Endpoint: "endpoint", + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "key", + }}, + }, + res{ + targets: []Target{ + &query.ExecutionTarget{ + InstanceID: instanceID, + ExecutionID: "executionID", + TargetID: "targetID", + TargetType: domain.TargetTypeWebhook, + Endpoint: "endpoint", + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "key", + }, + }, + contextInfo: &execution_rp.ContextInfoEvent{ + AggregateID: "aggID", + AggregateType: "session", + ResourceOwner: "resourceOwner", + InstanceID: "instanceID", + Version: "v1", + Sequence: 1, + EventType: "session.added", + CreatedAt: time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC).Format(time.RFC3339Nano), + UserID: userID, + EventPayload: []byte(`{"ID":"","Seq":1,"Pos":0,"Creation":"2024-01-01T01:01:01.000000001Z"}`), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request, err := NewRequest(tt.args.event, tt.args.targets) + if tt.res.wantErr { + assert.Error(t, err) + assert.Nil(t, request) + return + } + assert.NoError(t, err) + targets, err := TargetsFromRequest(request) + assert.NoError(t, err) + assert.Equal(t, tt.res.targets, targets) + assert.Equal(t, tt.res.contextInfo, execution_rp.ContextInfoFromRequest(request)) + }) + } +} + +func Test_groupsFromEventType(t *testing.T) { + type args struct { + eventType eventstore.EventType + } + type res struct { + groups []string + } + tests := []struct { + name string + args args + res res + }{ + { + "user human mfa init skipped, ok", + args{ + eventType: user.HumanMFAInitSkippedType, + }, + res{ + groups: []string{ + "user.human.mfa.init.skipped", + "user.human.mfa.init.*", + "user.human.mfa.*", + "user.human.*", + "user.*", + }, + }, + }, + { + "session added, ok", + args{ + eventType: session.AddedType, + }, + res{ + groups: []string{ + "session.added", + "session.*", + }, + }, + }, + { + "user added, ok", + args{ + eventType: user.HumanAddedType, + }, + res{ + groups: []string{ + "user.human.added", + "user.human.*", + "user.*", + }, + }, + }, + { + "execution set, ok", + args{ + eventType: execution_rp.SetEventV2Type, + }, + res{ + groups: []string{ + "execution.v2.set", + "execution.v2.*", + "execution.*", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.res.groups, groupsFromEventType(string(tt.args.eventType))) + }) + } +} + +func Test_idsForEventType(t *testing.T) { + type args struct { + eventType eventstore.EventType + } + type res struct { + groups []string + } + tests := []struct { + name string + args args + res res + }{ + { + "session added, ok", + args{ + eventType: session.AddedType, + }, + res{ + groups: []string{ + "event/session.added", + "event/session.*", + "event", + }, + }, + }, + { + "user added, ok", + args{ + eventType: user.HumanAddedType, + }, + res{ + groups: []string{ + "event/user.human.added", + "event/user.human.*", + "event/user.*", + "event", + }, + }, + }, + { + "execution set, ok", + args{ + eventType: execution_rp.SetEventV2Type, + }, + res{ + groups: []string{ + "event/execution.v2.set", + "event/execution.v2.*", + "event/execution.*", + "event", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.res.groups, idsForEventType(string(tt.args.eventType))) + }) + } +} + +func TestActionProjection_reduces(t *testing.T) { + tests := []struct { + name string + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) + }{ + { + name: "reduce, action, error", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { + queries.EXPECT().TargetsByExecutionID(gomock.Any(), gomock.Any()).Return(nil, zerrors.ThrowInternal(nil, "QUERY-37ardr0pki", "Errors.Query.CloseRows")) + return fields{ + queries: queries, + queue: q, + }, args{ + event: &action.AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: eventID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: action.AddedEventType, + Data: []byte(eventData), + EditorUser: userID, + Seq: 1, + AggregateType: action.AggregateType, + Version: action.AggregateVersion, + }), + Name: "name", + Script: "name(){}", + Timeout: 3 * time.Second, + AllowedToFail: true, + }, + mapper: action.AddedEventMapper, + }, want{ + err: func(tt assert.TestingT, err error, i ...interface{}) bool { + return errors.Is(err, zerrors.ThrowInternal(nil, "QUERY-37ardr0pki", "Errors.Query.CloseRows")) + }, + } + }, + }, + + { + name: "reduce, action, none", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { + queries.EXPECT().TargetsByExecutionID(gomock.Any(), gomock.Any()).Return([]*query.ExecutionTarget{}, nil) + return fields{ + queries: queries, + queue: q, + }, args{ + event: &action.AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: eventID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: action.AddedEventType, + Data: []byte(eventData), + EditorUser: userID, + Seq: 1, + AggregateType: action.AggregateType, + Version: action.AggregateVersion, + }), + Name: "name", + Script: "name(){}", + Timeout: 3 * time.Second, + AllowedToFail: true, + }, + mapper: action.AddedEventMapper, + }, want{ + noOperation: true, + } + }, + }, + { + name: "reduce, action, single", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { + targets := mockTargets(1) + queries.EXPECT().TargetsByExecutionID(gomock.Any(), gomock.Any()).Return(targets, nil) + createdAt := time.Now().UTC() + q.EXPECT().Insert( + gomock.Any(), + &execution_rp.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: createdAt, + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + TargetsData: mockTargetsToBytes(targets), + }, + gomock.Any(), + ).Return(nil) + return fields{ + queries: queries, + queue: q, + }, args{ + event: &action.AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: eventID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: createdAt, + Typ: action.AddedEventType, + Data: []byte(eventData), + EditorUser: userID, + Seq: 1, + AggregateType: action.AggregateType, + Version: action.AggregateVersion, + }), + Name: "name", + Script: "name(){}", + Timeout: 3 * time.Second, + AllowedToFail: true, + }, + mapper: action.AddedEventMapper, + }, w + }, + }, + { + name: "reduce, action, multiple", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { + targets := mockTargets(3) + queries.EXPECT().TargetsByExecutionID(gomock.Any(), gomock.Any()).Return(targets, nil) + createdAt := time.Now().UTC() + q.EXPECT().Insert( + gomock.Any(), + &execution_rp.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: createdAt, + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + TargetsData: mockTargetsToBytes(targets), + }, + gomock.Any(), + ).Return(nil) + return fields{ + queries: queries, + queue: q, + }, args{ + event: &action.AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: eventID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: createdAt, + Typ: action.AddedEventType, + Data: []byte(eventData), + EditorUser: userID, + Seq: 1, + AggregateType: action.AggregateType, + Version: action.AggregateVersion, + }), + Name: "name", + Script: "name(){}", + Timeout: 3 * time.Second, + AllowedToFail: true, + }, + mapper: action.AddedEventMapper, + }, w + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + queries := mock.NewMockQueries(ctrl) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + + event, err := a.mapper(a.event) + assert.NoError(t, err) + + stmt, err := newEventExecutionsHandler(queries, f).reduce(event) + if w.err != nil { + w.err(t, err) + return + } + assert.NoError(t, err) + + if w.noOperation { + assert.Nil(t, stmt.Execute) + return + } + err = stmt.Execute(nil, "") + if w.stmtErr != nil { + w.stmtErr(t, err) + return + } + assert.NoError(t, err) + }) + } +} + +func mockTarget() *query.ExecutionTarget { + return &query.ExecutionTarget{ + InstanceID: "instanceID", + ExecutionID: "executionID", + TargetID: "targetID", + TargetType: domain.TargetTypeWebhook, + Endpoint: "endpoint", + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "key", + } +} + +func mockTargets(count int) []*query.ExecutionTarget { + var targets []*query.ExecutionTarget + if count > 0 { + targets = make([]*query.ExecutionTarget, count) + for i := range targets { + targets[i] = mockTarget() + } + } + return targets +} + +func mockTargetsToBytes(targets []*query.ExecutionTarget) []byte { + data, _ := json.Marshal(targets) + return data +} + +func newEventExecutionsHandler(queries *mock.MockQueries, f fields) *eventHandler { + return &eventHandler{ + queue: f.queue, + query: queries, + } +} diff --git a/internal/execution/mock/queries.mock.go b/internal/execution/mock/queries.mock.go new file mode 100644 index 0000000000..ab7cf38a32 --- /dev/null +++ b/internal/execution/mock/queries.mock.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/execution (interfaces: Queries) +// +// Generated by this command: +// +// mockgen -package mock -destination ./mock/queries.mock.go github.com/zitadel/zitadel/internal/execution Queries +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + authz "github.com/zitadel/zitadel/internal/api/authz" + query "github.com/zitadel/zitadel/internal/query" + gomock "go.uber.org/mock/gomock" +) + +// MockQueries is a mock of Queries interface. +type MockQueries struct { + ctrl *gomock.Controller + recorder *MockQueriesMockRecorder +} + +// MockQueriesMockRecorder is the mock recorder for MockQueries. +type MockQueriesMockRecorder struct { + mock *MockQueries +} + +// NewMockQueries creates a new mock instance. +func NewMockQueries(ctrl *gomock.Controller) *MockQueries { + mock := &MockQueries{ctrl: ctrl} + mock.recorder = &MockQueriesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockQueries) EXPECT() *MockQueriesMockRecorder { + return m.recorder +} + +// InstanceByID mocks base method. +func (m *MockQueries) InstanceByID(arg0 context.Context, arg1 string) (authz.Instance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstanceByID", arg0, arg1) + ret0, _ := ret[0].(authz.Instance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InstanceByID indicates an expected call of InstanceByID. +func (mr *MockQueriesMockRecorder) InstanceByID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceByID", reflect.TypeOf((*MockQueries)(nil).InstanceByID), arg0, arg1) +} + +// TargetsByExecutionID mocks base method. +func (m *MockQueries) TargetsByExecutionID(arg0 context.Context, arg1 []string) ([]*query.ExecutionTarget, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TargetsByExecutionID", arg0, arg1) + ret0, _ := ret[0].([]*query.ExecutionTarget) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TargetsByExecutionID indicates an expected call of TargetsByExecutionID. +func (mr *MockQueriesMockRecorder) TargetsByExecutionID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TargetsByExecutionID", reflect.TypeOf((*MockQueries)(nil).TargetsByExecutionID), arg0, arg1) +} diff --git a/internal/execution/mock/queue.mock.go b/internal/execution/mock/queue.mock.go new file mode 100644 index 0000000000..c0e8d5fc7b --- /dev/null +++ b/internal/execution/mock/queue.mock.go @@ -0,0 +1,61 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/execution (interfaces: Queue) +// +// Generated by this command: +// +// mockgen -package mock -destination ./mock/queue.mock.go github.com/zitadel/zitadel/internal/execution Queue +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + river "github.com/riverqueue/river" + queue "github.com/zitadel/zitadel/internal/queue" + gomock "go.uber.org/mock/gomock" +) + +// MockQueue is a mock of Queue interface. +type MockQueue struct { + ctrl *gomock.Controller + recorder *MockQueueMockRecorder +} + +// MockQueueMockRecorder is the mock recorder for MockQueue. +type MockQueueMockRecorder struct { + mock *MockQueue +} + +// NewMockQueue creates a new mock instance. +func NewMockQueue(ctrl *gomock.Controller) *MockQueue { + mock := &MockQueue{ctrl: ctrl} + mock.recorder = &MockQueueMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockQueue) EXPECT() *MockQueueMockRecorder { + return m.recorder +} + +// Insert mocks base method. +func (m *MockQueue) Insert(arg0 context.Context, arg1 river.JobArgs, arg2 ...queue.InsertOpt) error { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Insert", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockQueueMockRecorder) Insert(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...) +} diff --git a/internal/execution/projections.go b/internal/execution/projections.go new file mode 100644 index 0000000000..d16d7c6fca --- /dev/null +++ b/internal/execution/projections.go @@ -0,0 +1,36 @@ +package execution + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/queue" +) + +var ( + projections []*handler.Handler +) + +func Register( + ctx context.Context, + executionsCustomConfig projection.CustomConfig, + workerConfig WorkerConfig, + queries *query.Queries, + eventTypes []string, + queue *queue.Queue, +) { + queue.ShouldStart() + projections = []*handler.Handler{ + NewEventHandler(ctx, projection.ApplyCustomConfig(executionsCustomConfig), eventTypes, eventstore.AggregateTypeFromEventType, queries, queue), + } + queue.AddWorkers(NewWorker(workerConfig)) +} + +func Start(ctx context.Context) { + for _, projection := range projections { + projection.Start(ctx) + } +} diff --git a/internal/execution/target_test.go b/internal/execution/target_test.go new file mode 100644 index 0000000000..8df480219d --- /dev/null +++ b/internal/execution/target_test.go @@ -0,0 +1,85 @@ +package execution + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "time" +) + +type testServer struct { + server *httptest.Server + called bool +} + +func (s *testServer) URL() string { + return s.server.URL +} + +func (s *testServer) Close() { + s.server.Close() +} + +func (s *testServer) Called() bool { + return s.called +} + +func testServerCall( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody interface{}, +) (string, func(), func() bool) { + server := &testServer{ + called: false, + } + + handler := func(w http.ResponseWriter, r *http.Request) { + server.called = true + if reqBody != nil { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + if respBody != nil { + w.Header().Set("Content-Type", "application/json") + resp, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := w.Write(resp); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } else { + if _, err := io.WriteString(w, "finished successfully"); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + } + + server.server = httptest.NewServer(http.HandlerFunc(handler)) + return server.URL(), server.Close, server.Called +} diff --git a/internal/execution/worker.go b/internal/execution/worker.go new file mode 100644 index 0000000000..105fa7d46e --- /dev/null +++ b/internal/execution/worker.go @@ -0,0 +1,90 @@ +package execution + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/riverqueue/river" + + "github.com/zitadel/zitadel/internal/query" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" +) + +type Worker struct { + river.WorkerDefaults[*exec_repo.Request] + + config WorkerConfig + now nowFunc +} + +// Timeout implements the Timeout-function of [river.Worker]. +// Maximum time a job can run before the context gets cancelled. +// The time can be shorter than the sum of target timeouts, this is expected behavior to not block the request indefinitely. +func (w *Worker) Timeout(*river.Job[*exec_repo.Request]) time.Duration { + return w.config.TransactionDuration +} + +// Work implements [river.Worker]. +func (w *Worker) Work(ctx context.Context, job *river.Job[*exec_repo.Request]) error { + ctx = ContextWithExecuter(ctx, job.Args.Aggregate) + + // if the event is too old, we can directly return as it will be removed anyway + if job.CreatedAt.Add(w.config.MaxTtl).Before(w.now()) { + return river.JobCancel(errors.New("event is too old")) + } + + targets, err := TargetsFromRequest(job.Args) + if err != nil { + // If we are not able to get the targets from the request, we can cancel the job, as we have nothing to call + return river.JobCancel(fmt.Errorf("unable to unmarshal targets because %w", err)) + } + + _, err = CallTargets(ctx, targets, exec_repo.ContextInfoFromRequest(job.Args)) + if err != nil { + // If there is an error returned from the targets, it means that the execution was interrupted + return river.JobCancel(fmt.Errorf("interruption during call of targets because %w", err)) + } + return nil +} + +// nowFunc makes [time.Now] mockable +type nowFunc func() time.Time + +type WorkerConfig struct { + Workers uint8 + TransactionDuration time.Duration + MaxTtl time.Duration +} + +func NewWorker( + config WorkerConfig, +) *Worker { + return &Worker{ + config: config, + now: time.Now, + } +} + +var _ river.Worker[*exec_repo.Request] = (*Worker)(nil) + +func (w *Worker) Register(workers *river.Workers, queues map[string]river.QueueConfig) { + river.AddWorker(workers, w) + queues[exec_repo.QueueName] = river.QueueConfig{ + MaxWorkers: int(w.config.Workers), + } +} + +func TargetsFromRequest(e *exec_repo.Request) ([]Target, error) { + var execTargets []*query.ExecutionTarget + if err := json.Unmarshal(e.TargetsData, &execTargets); err != nil { + return nil, err + } + targets := make([]Target, len(execTargets)) + for i, target := range execTargets { + targets[i] = target + } + return targets, nil +} diff --git a/internal/execution/worker_test.go b/internal/execution/worker_test.go new file mode 100644 index 0000000000..32f7879477 --- /dev/null +++ b/internal/execution/worker_test.go @@ -0,0 +1,288 @@ +package execution + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "testing" + "time" + + "github.com/riverqueue/river" + "github.com/riverqueue/river/rivertype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/execution/mock" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/action" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type fields struct { + queries *mock.MockQueries + queue *mock.MockQueue +} +type fieldsWorker struct { + now nowFunc +} +type args struct { + event eventstore.Event + mapper func(event eventstore.Event) (eventstore.Event, error) +} +type argsWorker struct { + job *river.Job[*exec_repo.Request] +} +type want struct { + noOperation bool + err assert.ErrorAssertionFunc + stmtErr assert.ErrorAssertionFunc +} +type wantWorker struct { + targets []*query.ExecutionTarget + sendStatusCode int + err assert.ErrorAssertionFunc +} + +func newExecutionWorker(f fieldsWorker) *Worker { + return &Worker{ + config: WorkerConfig{ + Workers: 1, + TransactionDuration: 5 * time.Second, + MaxTtl: 5 * time.Minute, + }, + now: f.now, + } +} + +const ( + userID = "user1" + orgID = "orgID" + instanceID = "instanceID" + eventID = "eventID" + eventData = `{"name":"name","script":"name(){}","timeout":3000000000,"allowedToFail":true}` +) + +func Test_handleEventExecution(t *testing.T) { + testNow := time.Now + tests := []struct { + name string + test func() (fieldsWorker, argsWorker, wantWorker) + }{ + { + "max TTL", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now().Add(-1 * time.Hour), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: eventID, + ResourceOwner: instanceID, + }, + Sequence: 1, + CreatedAt: time.Now().Add(-1 * time.Hour), + EventType: user.HumanInviteCodeAddedType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(1), + sendStatusCode: http.StatusOK, + err: func(tt assert.TestingT, err error, i ...interface{}) bool { + return errors.Is(err, new(river.JobCancelError)) + }, + } + }, + }, + { + "none", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: eventID, + ResourceOwner: instanceID, + }, + Sequence: 1, + CreatedAt: time.Now(), + EventType: user.HumanInviteCodeAddedType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(0), + sendStatusCode: http.StatusOK, + err: nil, + } + }, + }, + { + "single", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: time.Now().UTC(), + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(1), + sendStatusCode: http.StatusOK, + err: nil, + } + }, + }, + { + "single, failed 400", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: time.Now().UTC(), + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(1), + sendStatusCode: http.StatusBadRequest, + err: func(tt assert.TestingT, err error, i ...interface{}) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed")) + }, + } + }, + }, + { + "multiple", + func() (fieldsWorker, argsWorker, wantWorker) { + return fieldsWorker{ + now: testNow, + }, + argsWorker{ + job: &river.Job[*exec_repo.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: &exec_repo.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + Type: action.AggregateType, + Version: action.AggregateVersion, + ID: eventID, + ResourceOwner: orgID, + }, + Sequence: 1, + CreatedAt: time.Now().UTC(), + EventType: action.AddedEventType, + UserID: userID, + EventData: []byte(eventData), + }, + }, + }, + wantWorker{ + targets: mockTargets(3), + sendStatusCode: http.StatusOK, + err: nil, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, a, w := tt.test() + + closeFuncs := make([]func(), len(w.targets)) + calledFuncs := make([]func() bool, len(w.targets)) + for i := range w.targets { + url, closeF, calledF := testServerCall( + exec_repo.ContextInfoFromRequest(a.job.Args), + time.Second, + w.sendStatusCode, + nil, + ) + w.targets[i].Endpoint = url + closeFuncs[i] = closeF + calledFuncs[i] = calledF + } + + data, err := json.Marshal(w.targets) + require.NoError(t, err) + a.job.Args.TargetsData = data + + err = newExecutionWorker(f).Work( + authz.WithInstanceID(context.Background(), instanceID), + a.job, + ) + + if w.err != nil { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + for _, closeF := range closeFuncs { + closeF() + } + for _, calledF := range calledFuncs { + assert.True(t, calledF()) + } + }) + } +} diff --git a/internal/feature/feature.go b/internal/feature/feature.go index d9a2d6352d..389b750483 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -15,7 +15,7 @@ const ( KeyLegacyIntrospection KeyUserSchema KeyTokenExchange - KeyActions + KeyActionsDeprecated KeyImprovedPerformance KeyWebKey KeyDebugOIDCParentError @@ -24,6 +24,7 @@ const ( KeyEnableBackChannelLogout KeyLoginV2 KeyPermissionCheckV2 + KeyConsoleUseV2UserApi ) //go:generate enumer -type Level -transform snake -trimprefix Level @@ -45,7 +46,6 @@ type Features struct { LegacyIntrospection bool `json:"legacy_introspection,omitempty"` UserSchema bool `json:"user_schema,omitempty"` TokenExchange bool `json:"token_exchange,omitempty"` - Actions bool `json:"actions,omitempty"` ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` WebKey bool `json:"web_key,omitempty"` DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"` @@ -54,6 +54,7 @@ type Features struct { EnableBackChannelLogout bool `json:"enable_back_channel_logout,omitempty"` LoginV2 LoginV2 `json:"login_v2,omitempty"` PermissionCheckV2 bool `json:"permission_check_v2,omitempty"` + ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"` } type ImprovedPerformanceType int32 diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index 3a805df807..6466061718 100644 --- a/internal/feature/key_enumer.go +++ b/internal/feature/key_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2" +const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" -var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 197, 221, 247, 255, 274} +var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 197, 221, 247, 255, 274, 297} -const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2" +const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" func (i Key) String() string { if i < 0 || i >= Key(len(_KeyIndex)-1) { @@ -30,7 +30,7 @@ func _KeyNoOp() { _ = x[KeyLegacyIntrospection-(3)] _ = x[KeyUserSchema-(4)] _ = x[KeyTokenExchange-(5)] - _ = x[KeyActions-(6)] + _ = x[KeyActionsDeprecated-(6)] _ = x[KeyImprovedPerformance-(7)] _ = x[KeyWebKey-(8)] _ = x[KeyDebugOIDCParentError-(9)] @@ -39,9 +39,10 @@ func _KeyNoOp() { _ = x[KeyEnableBackChannelLogout-(12)] _ = x[KeyLoginV2-(13)] _ = x[KeyPermissionCheckV2-(14)] + _ = x[KeyConsoleUseV2UserApi-(15)] } -var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2} +var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActionsDeprecated, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} var _KeyNameToValueMap = map[string]Key{ _KeyName[0:11]: KeyUnspecified, @@ -56,8 +57,8 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[81:92]: KeyUserSchema, _KeyName[92:106]: KeyTokenExchange, _KeyLowerName[92:106]: KeyTokenExchange, - _KeyName[106:113]: KeyActions, - _KeyLowerName[106:113]: KeyActions, + _KeyName[106:113]: KeyActionsDeprecated, + _KeyLowerName[106:113]: KeyActionsDeprecated, _KeyName[113:133]: KeyImprovedPerformance, _KeyLowerName[113:133]: KeyImprovedPerformance, _KeyName[133:140]: KeyWebKey, @@ -74,6 +75,8 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[247:255]: KeyLoginV2, _KeyName[255:274]: KeyPermissionCheckV2, _KeyLowerName[255:274]: KeyPermissionCheckV2, + _KeyName[274:297]: KeyConsoleUseV2UserApi, + _KeyLowerName[274:297]: KeyConsoleUseV2UserApi, } var _KeyNames = []string{ @@ -92,6 +95,7 @@ var _KeyNames = []string{ _KeyName[221:247], _KeyName[247:255], _KeyName[255:274], + _KeyName[274:297], } // KeyString retrieves an enum value from the enum constants string name. diff --git a/internal/id/sonyflake.go b/internal/id/sonyflake.go index 609570436a..cc7086aa66 100644 --- a/internal/id/sonyflake.go +++ b/internal/id/sonyflake.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" "hash/fnv" - "io/ioutil" + "io" "net" "net/http" "os" @@ -73,7 +73,7 @@ func privateIPv4() (net.IP, error) { } } - //change: use "POD_IP" + // change: use "POD_IP" ip := net.ParseIP(os.Getenv("POD_IP")) if ip == nil { return nil, errors.New("no private ip address") @@ -140,7 +140,7 @@ func machineID() (uint16, error) { } logging.WithFields("errors", strings.Join(errors, ", ")).Panic("none of the enabled methods for identifying the machine succeeded") - //this return will never happen because of panic one line before + // this return will never happen because of panic one line before return 0, nil } @@ -200,7 +200,7 @@ func metadataWebhookID() (uint16, error) { if resp.StatusCode >= 400 && resp.StatusCode < 600 { return 0, fmt.Errorf("metadata endpoint returned an unsuccessful status code %d", resp.StatusCode) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return 0, err } diff --git a/internal/idp/providers/apple/session.go b/internal/idp/providers/apple/session.go index 5e9143f050..eee68fa2a5 100644 --- a/internal/idp/providers/apple/session.go +++ b/internal/idp/providers/apple/session.go @@ -17,6 +17,10 @@ type Session struct { UserFormValue string } +func NewSession(provider *Provider, code, userFormValue string) *Session { + return &Session{Session: oidc.NewSession(provider.Provider, code, nil), UserFormValue: userFormValue} +} + type userFormValue struct { Name userNamesFormValue `json:"name,omitempty" schema:"name"` } diff --git a/internal/idp/providers/azuread/azuread.go b/internal/idp/providers/azuread/azuread.go index 65f38ede5b..a15f793e37 100644 --- a/internal/idp/providers/azuread/azuread.go +++ b/internal/idp/providers/azuread/azuread.go @@ -152,6 +152,10 @@ func ensureMinimalScope(scopes []string) []string { return scopes } +func (p *Provider) User() idp.User { + return p.Provider.User() +} + // User represents the structure return on the userinfo endpoint and implements the [idp.User] interface // // AzureAD does not return an `email_verified` claim. diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go index a9d8df2e8c..4b0a6fb844 100644 --- a/internal/idp/providers/azuread/session.go +++ b/internal/idp/providers/azuread/session.go @@ -20,6 +20,10 @@ type Session struct { OAuthSession *oauth.Session } +func NewSession(provider *Provider, code string) *Session { + return &Session{Provider: provider, Code: code} +} + // GetAuth implements the [idp.Provider] interface by calling the wrapped [oauth.Session]. func (s *Session) GetAuth(ctx context.Context) (content string, redirect bool) { return s.oauth().GetAuth(ctx) @@ -39,6 +43,11 @@ func (s *Session) RetrievePreviousID() (string, error) { return userinfo.Subject, nil } +// PersistentParameters implements the [idp.Session] interface. +func (s *Session) PersistentParameters() map[string]any { + return nil +} + // FetchUser implements the [idp.Session] interface. // It will execute an OAuth 2.0 code exchange if needed to retrieve the access token, // call the specified userEndpoint and map the received information into an [idp.User]. diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 54fcc039eb..6df08a6998 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -30,11 +30,20 @@ type Session struct { Tokens *oidc.Tokens[*oidc.IDTokenClaims] } +func NewSession(provider *Provider, tokens *oidc.Tokens[*oidc.IDTokenClaims]) *Session { + return &Session{Provider: provider, Tokens: tokens} +} + // GetAuth implements the [idp.Session] interface. func (s *Session) GetAuth(ctx context.Context) (string, bool) { return idp.Redirect(s.AuthURL) } +// PersistentParameters implements the [idp.Session] interface. +func (s *Session) PersistentParameters() map[string]any { + return nil +} + // FetchUser implements the [idp.Session] interface. // It will map the received idToken into an [idp.User]. func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { diff --git a/internal/idp/providers/ldap/ldap.go b/internal/idp/providers/ldap/ldap.go index a42ebf2379..567356d417 100644 --- a/internal/idp/providers/ldap/ldap.go +++ b/internal/idp/providers/ldap/ldap.go @@ -23,6 +23,7 @@ type Provider struct { userObjectClasses []string userFilters []string timeout time.Duration + rootCA []byte loginUrl string @@ -185,6 +186,7 @@ func New( userObjectClasses []string, userFilters []string, timeout time.Duration, + rootCA []byte, loginUrl string, options ...ProviderOpts, ) *Provider { @@ -199,6 +201,7 @@ func New( userObjectClasses: userObjectClasses, userFilters: userFilters, timeout: timeout, + rootCA: rootCA, loginUrl: loginUrl, } for _, option := range options { diff --git a/internal/idp/providers/ldap/ldap_test.go b/internal/idp/providers/ldap/ldap_test.go index d00680da1a..8b9f08defa 100644 --- a/internal/idp/providers/ldap/ldap_test.go +++ b/internal/idp/providers/ldap/ldap_test.go @@ -18,11 +18,13 @@ func TestProvider_Options(t *testing.T) { userObjectClasses []string userFilters []string timeout time.Duration + rootCA []byte loginUrl string opts []ProviderOpts } type want struct { name string + rootCA []byte startTls bool linkingAllowed bool creationAllowed bool @@ -114,6 +116,7 @@ func TestProvider_Options(t *testing.T) { userObjectClasses: []string{"object"}, userFilters: []string{"filter"}, timeout: 30 * time.Second, + rootCA: []byte("certificate"), loginUrl: "url", opts: []ProviderOpts{ WithoutStartTLS(), @@ -138,6 +141,7 @@ func TestProvider_Options(t *testing.T) { }, want: want{ name: "ldap", + rootCA: []byte("certificate"), startTls: false, linkingAllowed: true, creationAllowed: true, @@ -172,11 +176,13 @@ func TestProvider_Options(t *testing.T) { tt.fields.userObjectClasses, tt.fields.userFilters, tt.fields.timeout, + tt.fields.rootCA, tt.fields.loginUrl, tt.fields.opts..., ) a.Equal(tt.want.name, provider.Name()) + a.Equal(tt.want.rootCA, provider.rootCA) a.Equal(tt.want.startTls, provider.startTLS) a.Equal(tt.want.linkingAllowed, provider.IsLinkingAllowed()) a.Equal(tt.want.creationAllowed, provider.IsCreationAllowed()) diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index c3ca5c6364..0a6a87ba3d 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -3,6 +3,7 @@ package ldap import ( "context" "crypto/tls" + "crypto/x509" "encoding/base64" "errors" "net" @@ -21,6 +22,7 @@ import ( var ErrNoSingleUser = errors.New("user does not exist or too many entries returned") var ErrFailedLogin = errors.New("user failed to login") +var ErrUnableToAppendRootCA = errors.New("unable to append rootCA") var _ idp.Session = (*Session)(nil) @@ -32,11 +34,21 @@ type Session struct { Entry *ldap.Entry } +func NewSession(provider *Provider, username, password string) *Session { + return &Session{Provider: provider, User: username, Password: password} +} + // GetAuth implements the [idp.Session] interface. func (s *Session) GetAuth(ctx context.Context) (string, bool) { return idp.Redirect(s.loginUrl) } +// PersistentParameters implements the [idp.Session] interface. +func (s *Session) PersistentParameters() map[string]any { + return nil +} + +// FetchUser implements the [idp.Session] interface. func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) { var user *ldap.Entry for _, server := range s.Provider.servers { @@ -49,7 +61,9 @@ func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) { s.Provider.userObjectClasses, s.Provider.userFilters, s.User, - s.Password, s.Provider.timeout) + s.Password, + s.Provider.timeout, + s.Provider.rootCA) // If there were invalid credentials or multiple users with the credentials cancel process if err != nil && (errors.Is(err, ErrFailedLogin) || errors.Is(err, ErrNoSingleUser)) { return nil, err @@ -94,8 +108,9 @@ func tryBind( username string, password string, timeout time.Duration, + rootCA []byte, ) (*ldap.Entry, error) { - conn, err := getConnection(server, startTLS, timeout) + conn, err := getConnection(server, startTLS, timeout, rootCA) if err != nil { return nil, err } @@ -114,6 +129,7 @@ func tryBind( username, password, timeout, + rootCA, ) } @@ -121,21 +137,37 @@ func getConnection( server string, startTLS bool, timeout time.Duration, + rootCA []byte, ) (*ldap.Conn, error) { if timeout == 0 { timeout = ldap.DefaultTimeout } - conn, err := ldap.DialURL(server, ldap.DialWithDialer(&net.Dialer{Timeout: timeout})) - if err != nil { - return nil, err - } + dialer := make([]ldap.DialOpt, 1, 2) + dialer[0] = ldap.DialWithDialer(&net.Dialer{Timeout: timeout}) u, err := url.Parse(server) if err != nil { return nil, err } - if u.Scheme == "ldaps" && startTLS { + + if u.Scheme == "ldaps" && len(rootCA) > 0 { + rootCAs := x509.NewCertPool() + if ok := rootCAs.AppendCertsFromPEM(rootCA); !ok { + return nil, ErrUnableToAppendRootCA + } + + dialer = append(dialer, ldap.DialWithTLSConfig(&tls.Config{ + RootCAs: rootCAs, + })) + } + + conn, err := ldap.DialURL(server, dialer...) + if err != nil { + return nil, err + } + + if u.Scheme == "ldap" && startTLS { err = conn.StartTLS(&tls.Config{ServerName: u.Host}) if err != nil { return nil, err @@ -153,6 +185,7 @@ func trySearchAndUserBind( username string, password string, timeout time.Duration, + rootCA []byte, ) (*ldap.Entry, error) { searchQuery := queriesAndToSearchQuery( objectClassesToSearchQuery(objectClasses), diff --git a/internal/idp/providers/oauth/oauth2.go b/internal/idp/providers/oauth/oauth2.go index 910ceb1b9c..a790c550f5 100644 --- a/internal/idp/providers/oauth/oauth2.go +++ b/internal/idp/providers/oauth/oauth2.go @@ -18,11 +18,12 @@ type Provider struct { options []rp.Option name string userEndpoint string - userMapper func() idp.User + user func() idp.User isLinkingAllowed bool isCreationAllowed bool isAutoCreation bool isAutoUpdate bool + generateVerifier func() string } type ProviderOpts func(provider *Provider) @@ -64,11 +65,12 @@ func WithRelyingPartyOption(option rp.Option) ProviderOpts { } // New creates a generic OAuth 2.0 provider -func New(config *oauth2.Config, name, userEndpoint string, userMapper func() idp.User, options ...ProviderOpts) (provider *Provider, err error) { +func New(config *oauth2.Config, name, userEndpoint string, user func() idp.User, options ...ProviderOpts) (provider *Provider, err error) { provider = &Provider{ - name: name, - userEndpoint: userEndpoint, - userMapper: userMapper, + name: name, + userEndpoint: userEndpoint, + user: user, + generateVerifier: oauth2.GenerateVerifier, } for _, option := range options { option(provider) @@ -99,8 +101,15 @@ func (p *Provider) BeginAuth(ctx context.Context, state string, params ...idp.Pa if !loginHintSet { opts = append(opts, rp.WithPrompt(oidc.PromptSelectAccount)) } + + var codeVerifier string + if p.RelyingParty.IsPKCE() { + codeVerifier = p.generateVerifier() + opts = append(opts, rp.WithCodeChallenge(oidc.NewSHACodeChallenge(codeVerifier))) + } + url := rp.AuthURL(state, p.RelyingParty, opts...) - return &Session{AuthURL: url, Provider: p}, nil + return &Session{AuthURL: url, Provider: p, CodeVerifier: codeVerifier}, nil } func loginHint(hint string) rp.AuthURLOpt { @@ -128,3 +137,7 @@ func (p *Provider) IsAutoCreation() bool { func (p *Provider) IsAutoUpdate() bool { return p.isAutoUpdate } + +func (p *Provider) User() idp.User { + return p.user() +} diff --git a/internal/idp/providers/oauth/oauth2_test.go b/internal/idp/providers/oauth/oauth2_test.go index 814a7ac9c2..984315ac1f 100644 --- a/internal/idp/providers/oauth/oauth2_test.go +++ b/internal/idp/providers/oauth/oauth2_test.go @@ -18,6 +18,7 @@ func TestProvider_BeginAuth(t *testing.T) { name string userEndpoint string userMapper func() idp.User + options []ProviderOpts } tests := []struct { name string @@ -25,7 +26,7 @@ func TestProvider_BeginAuth(t *testing.T) { want idp.Session }{ { - name: "successful auth", + name: "successful auth without PKCE", fields: fields{ config: &oauth2.Config{ ClientID: "clientID", @@ -40,14 +41,40 @@ func TestProvider_BeginAuth(t *testing.T) { }, want: &Session{AuthURL: "https://oauth2.com/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=user&state=testState"}, }, + { + name: "successful auth with PKCE", + fields: fields{ + config: &oauth2.Config{ + ClientID: "clientID", + ClientSecret: "clientSecret", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://oauth2.com/authorize", + TokenURL: "https://oauth2.com/token", + }, + RedirectURL: "redirectURI", + Scopes: []string{"user"}, + }, + options: []ProviderOpts{ + WithLinkingAllowed(), + WithCreationAllowed(), + WithAutoCreation(), + WithAutoUpdate(), + WithRelyingPartyOption(rp.WithPKCE(nil)), + }, + }, + want: &Session{AuthURL: "https://oauth2.com/authorize?client_id=clientID&code_challenge=2ZoH_a01aprzLkwVbjlPsBo4m8mJ_zOKkaDqYM7Oh5w&code_challenge_method=S256&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=user&state=testState"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := assert.New(t) r := require.New(t) - provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper) + provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, tt.fields.options...) r.NoError(err) + provider.generateVerifier = func() string { + return "pkceOAuthVerifier" + } ctx := context.Background() session, err := provider.BeginAuth(ctx, "testState") diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index 065ad3b213..247a7f8710 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -14,26 +14,44 @@ import ( var ErrCodeMissing = errors.New("no auth code provided") +const ( + CodeVerifier = "codeVerifier" +) + var _ idp.Session = (*Session)(nil) // Session is the [idp.Session] implementation for the OAuth2.0 provider. type Session struct { - AuthURL string - Code string - Tokens *oidc.Tokens[*oidc.IDTokenClaims] + AuthURL string + CodeVerifier string + Code string + Tokens *oidc.Tokens[*oidc.IDTokenClaims] Provider *Provider } +func NewSession(provider *Provider, code string, idpArguments map[string]any) *Session { + verifier, _ := idpArguments[CodeVerifier].(string) + return &Session{Provider: provider, Code: code, CodeVerifier: verifier} +} + // GetAuth implements the [idp.Session] interface. func (s *Session) GetAuth(ctx context.Context) (string, bool) { return idp.Redirect(s.AuthURL) } +// PersistentParameters implements the [idp.Session] interface. +func (s *Session) PersistentParameters() map[string]any { + if s.CodeVerifier == "" { + return nil + } + return map[string]any{CodeVerifier: s.CodeVerifier} +} + // FetchUser implements the [idp.Session] interface. // It will execute an OAuth 2.0 code exchange if needed to retrieve the access token, // call the specified userEndpoint and map the received information into an [idp.User]. -func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { +func (s *Session) FetchUser(ctx context.Context) (_ idp.User, err error) { if s.Tokens == nil { if err = s.authorize(ctx); err != nil { return nil, err @@ -44,18 +62,22 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return nil, err } req.Header.Set("authorization", s.Tokens.TokenType+" "+s.Tokens.AccessToken) - mapper := s.Provider.userMapper() - if err := httphelper.HttpRequest(s.Provider.RelyingParty.HttpClient(), req, &mapper); err != nil { + user := s.Provider.User() + if err := httphelper.HttpRequest(s.Provider.RelyingParty.HttpClient(), req, &user); err != nil { return nil, err } - return mapper, nil + return user, nil } func (s *Session) authorize(ctx context.Context) (err error) { if s.Code == "" { return ErrCodeMissing } - s.Tokens, err = rp.CodeExchange[*oidc.IDTokenClaims](ctx, s.Code, s.Provider.RelyingParty) + var opts []rp.CodeExchangeOpt + if s.CodeVerifier != "" { + opts = append(opts, rp.WithCodeVerifier(s.CodeVerifier)) + } + s.Tokens, err = rp.CodeExchange[*oidc.IDTokenClaims](ctx, s.Code, s.Provider.RelyingParty, opts...) return err } diff --git a/internal/idp/providers/oidc/oidc.go b/internal/idp/providers/oidc/oidc.go index f122230c3a..cd3ac764fd 100644 --- a/internal/idp/providers/oidc/oidc.go +++ b/internal/idp/providers/oidc/oidc.go @@ -24,6 +24,7 @@ type Provider struct { useIDToken bool userInfoMapper func(info *oidc.UserInfo) idp.User authOptions []func(bool) rp.AuthURLOpt + generateVerifier func() string } type ProviderOpts func(provider *Provider) @@ -102,8 +103,9 @@ var DefaultMapper UserInfoMapper = func(info *oidc.UserInfo) idp.User { // New creates a generic OIDC provider func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, userInfoMapper UserInfoMapper, options ...ProviderOpts) (provider *Provider, err error) { provider = &Provider{ - name: name, - userInfoMapper: userInfoMapper, + name: name, + userInfoMapper: userInfoMapper, + generateVerifier: oauth2.GenerateVerifier, } for _, option := range options { option(provider) @@ -150,8 +152,15 @@ func (p *Provider) BeginAuth(ctx context.Context, state string, params ...idp.Pa opts = append(opts, opt) } } + + var codeVerifier string + if p.RelyingParty.IsPKCE() { + codeVerifier = p.generateVerifier() + opts = append(opts, rp.WithCodeChallenge(oidc.NewSHACodeChallenge(codeVerifier))) + } + url := rp.AuthURL(state, p.RelyingParty, opts...) - return &Session{AuthURL: url, Provider: p}, nil + return &Session{AuthURL: url, Provider: p, CodeVerifier: codeVerifier}, nil } func loginHint(hint string) rp.AuthURLOpt { diff --git a/internal/idp/providers/oidc/oidc_test.go b/internal/idp/providers/oidc/oidc_test.go index d510bf15c2..a46f09f13f 100644 --- a/internal/idp/providers/oidc/oidc_test.go +++ b/internal/idp/providers/oidc/oidc_test.go @@ -31,7 +31,7 @@ func TestProvider_BeginAuth(t *testing.T) { want idp.Session }{ { - name: "successful auth", + name: "successful auth without PKCE", fields: fields{ name: "oidc", issuer: "https://issuer.com", @@ -55,6 +55,31 @@ func TestProvider_BeginAuth(t *testing.T) { }, want: &Session{AuthURL: "https://issuer.com/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState"}, }, + { + name: "successful auth with PKCE", + fields: fields{ + name: "oidc", + issuer: "https://issuer.com", + clientID: "clientID", + clientSecret: "clientSecret", + redirectURI: "redirectURI", + scopes: []string{"openid"}, + userMapper: DefaultMapper, + httpMock: func(issuer string) { + gock.New(issuer). + Get(oidc.DiscoveryEndpoint). + Reply(200). + JSON(&oidc.DiscoveryConfiguration{ + Issuer: issuer, + AuthorizationEndpoint: issuer + "/authorize", + TokenEndpoint: issuer + "/token", + UserinfoEndpoint: issuer + "/userinfo", + }) + }, + opts: []ProviderOpts{WithSelectAccount(), WithRelyingPartyOption(rp.WithPKCE(nil))}, + }, + want: &Session{AuthURL: "https://issuer.com/authorize?client_id=clientID&code_challenge=2ZoH_a01aprzLkwVbjlPsBo4m8mJ_zOKkaDqYM7Oh5w&code_challenge_method=S256&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -65,6 +90,9 @@ func TestProvider_BeginAuth(t *testing.T) { provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.fields.opts...) r.NoError(err) + provider.generateVerifier = func() string { + return "pkceOAuthVerifier" + } ctx := context.Background() session, err := provider.BeginAuth(ctx, "testState") diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index bd6303f2e5..b17a3b0a0b 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -10,6 +10,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/idp" + "github.com/zitadel/zitadel/internal/idp/providers/oauth" ) var ErrCodeMissing = errors.New("no auth code provided") @@ -18,10 +19,16 @@ var _ idp.Session = (*Session)(nil) // Session is the [idp.Session] implementation for the OIDC provider. type Session struct { - Provider *Provider - AuthURL string - Code string - Tokens *oidc.Tokens[*oidc.IDTokenClaims] + Provider *Provider + AuthURL string + CodeVerifier string + Code string + Tokens *oidc.Tokens[*oidc.IDTokenClaims] +} + +func NewSession(provider *Provider, code string, idpArguments map[string]any) *Session { + verifier, _ := idpArguments[oauth.CodeVerifier].(string) + return &Session{Provider: provider, Code: code, CodeVerifier: verifier} } // GetAuth implements the [idp.Session] interface. @@ -29,6 +36,14 @@ func (s *Session) GetAuth(ctx context.Context) (string, bool) { return idp.Redirect(s.AuthURL) } +// PersistentParameters implements the [idp.Session] interface. +func (s *Session) PersistentParameters() map[string]any { + if s.CodeVerifier == "" { + return nil + } + return map[string]any{oauth.CodeVerifier: s.CodeVerifier} +} + // FetchUser implements the [idp.Session] interface. // It will execute an OIDC code exchange if needed to retrieve the tokens, // call the userinfo endpoint and map the received information into an [idp.User]. @@ -61,7 +76,11 @@ func (s *Session) Authorize(ctx context.Context) (err error) { if s.Code == "" { return ErrCodeMissing } - s.Tokens, err = rp.CodeExchange[*oidc.IDTokenClaims](ctx, s.Code, s.Provider.RelyingParty) + var opts []rp.CodeExchangeOpt + if s.CodeVerifier != "" { + opts = append(opts, rp.WithCodeVerifier(s.CodeVerifier)) + } + s.Tokens, err = rp.CodeExchange[*oidc.IDTokenClaims](ctx, s.Code, s.Provider.RelyingParty, opts...) return err } diff --git a/internal/idp/providers/saml/session.go b/internal/idp/providers/saml/session.go index 49a04e49cb..b0748d33a3 100644 --- a/internal/idp/providers/saml/session.go +++ b/internal/idp/providers/saml/session.go @@ -60,6 +60,11 @@ func (s *Session) GetAuth(ctx context.Context) (string, bool) { return idp.Form(resp.content.String()) } +// PersistentParameters implements the [idp.Session] interface. +func (s *Session) PersistentParameters() map[string]any { + return nil +} + // FetchUser implements the [idp.Session] interface. func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { if s.RequestID == "" || s.Request == nil { diff --git a/internal/idp/session.go b/internal/idp/session.go index 6d6519a54c..ab54bcabaa 100644 --- a/internal/idp/session.go +++ b/internal/idp/session.go @@ -7,6 +7,7 @@ import ( // Session is the minimal implementation for a session of a 3rd party authentication [Provider] type Session interface { GetAuth(ctx context.Context) (content string, redirect bool) + PersistentParameters() map[string]any FetchUser(ctx context.Context) (User, error) } diff --git a/internal/integration/action.go b/internal/integration/action.go new file mode 100644 index 0000000000..e849b5c21c --- /dev/null +++ b/internal/integration/action.go @@ -0,0 +1,163 @@ +package integration + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "sync" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +type server struct { + server *httptest.Server + mu sync.Mutex + called int +} + +func (s *server) URL() string { + return s.server.URL +} + +func (s *server) Close() { + s.server.Close() +} + +func (s *server) Called() int { + s.mu.Lock() + called := s.called + s.mu.Unlock() + return called +} + +func (s *server) Increase() { + s.mu.Lock() + s.called++ + s.mu.Unlock() +} + +func (s *server) ResetCalled() { + s.mu.Lock() + s.called = 0 + s.mu.Unlock() +} + +func TestServerCall( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody interface{}, +) (url string, closeF func(), calledF func() int, resetCalledF func()) { + server := &server{ + called: 0, + } + + handler := func(w http.ResponseWriter, r *http.Request) { + server.Increase() + if reqBody != nil { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + if respBody != nil { + w.Header().Set("Content-Type", "application/json") + resp, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := io.Writer.Write(w, resp); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } else { + if _, err := io.WriteString(w, "finished successfully"); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + } + + server.server = httptest.NewServer(http.HandlerFunc(handler)) + return server.URL(), server.Close, server.Called, server.ResetCalled +} + +func TestServerCallProto( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody proto.Message, +) (url string, closeF func(), calledF func() int, resetCalledF func()) { + server := &server{ + called: 0, + } + + handler := func(w http.ResponseWriter, r *http.Request) { + server.Increase() + if reqBody != nil { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + if respBody != nil { + w.Header().Set("Content-Type", "application/json") + resp, err := protojson.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := io.Writer.Write(w, resp); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } else { + if _, err := io.WriteString(w, "finished successfully"); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + } + + server.server = httptest.NewServer(http.HandlerFunc(handler)) + return server.URL(), server.Close, server.Called, server.ResetCalled +} diff --git a/internal/integration/assert.go b/internal/integration/assert.go index 77d7558b55..22432a3a84 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -19,6 +19,7 @@ import ( type Details interface { comparable GetSequence() uint64 + GetCreationDate() *timestamppb.Timestamp GetChangeDate() *timestamppb.Timestamp GetResourceOwner() string } @@ -62,6 +63,12 @@ func AssertDetails[D Details, M DetailsMsg[D]](t assert.TestingT, expected, actu assert.NotZero(t, gotDetails.GetSequence()) + if wantDetails.GetCreationDate() != nil { + wantCreationDate := time.Now() + gotCreationDate := gotDetails.GetCreationDate().AsTime() + assert.WithinRange(t, gotCreationDate, wantCreationDate.Add(-time.Minute), wantCreationDate.Add(time.Minute)) + } + if wantDetails.GetChangeDate() != nil { wantChangeDate := time.Now() gotChangeDate := gotDetails.GetChangeDate().AsTime() diff --git a/internal/integration/client.go b/internal/integration/client.go index cefaf0ef42..e82a6bec55 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -3,6 +3,7 @@ package integration import ( "context" "fmt" + "sync" "testing" "time" @@ -18,6 +19,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration/scim" + action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/auth" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" @@ -31,10 +33,8 @@ import ( oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" - webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" @@ -43,6 +43,7 @@ import ( user_pb "github.com/zitadel/zitadel/pkg/grpc/user" user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + webkey_v2beta "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) type Client struct { @@ -60,11 +61,11 @@ type Client struct { OIDCv2 oidc_pb.OIDCServiceClient OrgV2beta org_v2beta.OrganizationServiceClient OrgV2 org.OrganizationServiceClient - ActionV3Alpha action.ZITADELActionsClient + ActionV2beta action.ActionServiceClient FeatureV2beta feature_v2beta.FeatureServiceClient FeatureV2 feature.FeatureServiceClient UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient - WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient + WebKeyV2Beta webkey_v2beta.WebKeyServiceClient IDPv2 idp_pb.IdentityProviderServiceClient UserV3Alpha user_v3alpha.ZITADELUsersClient SAMLv2 saml_pb.SAMLServiceClient @@ -93,11 +94,11 @@ func newClient(ctx context.Context, target string) (*Client, error) { OIDCv2: oidc_pb.NewOIDCServiceClient(cc), OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), OrgV2: org.NewOrganizationServiceClient(cc), - ActionV3Alpha: action.NewZITADELActionsClient(cc), + ActionV2beta: action.NewActionServiceClient(cc), FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), FeatureV2: feature.NewFeatureServiceClient(cc), UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc), - WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc), + WebKeyV2Beta: webkey_v2beta.NewWebKeyServiceClient(cc), IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), SAMLv2: saml_pb.NewSAMLServiceClient(cc), @@ -157,6 +158,7 @@ func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRes }, }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } @@ -181,6 +183,7 @@ func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHuman }, }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } @@ -212,9 +215,26 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) * TotpSecret: gu.Ptr(secret), }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } +// TriggerUserByID makes sure the user projection gets triggered after creation. +func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) { + var wg sync.WaitGroup + wg.Add(len(users)) + for _, user := range users { + go func(user string) { + defer wg.Done() + _, err := i.Client.UserV2.GetUserByID(ctx, &user_v2.GetUserByIDRequest{ + UserId: user, + }) + logging.OnError(err).Warn("get user by ID for trigger failed") + }(user) + } + wg.Wait() +} + func (i *Instance) CreateOrganization(ctx context.Context, name, adminEmail string) *org.AddOrganizationResponse { resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ Name: name, @@ -238,6 +258,13 @@ func (i *Instance) CreateOrganization(ctx context.Context, name, adminEmail stri }, }) logging.OnError(err).Panic("create org") + + users := make([]string, len(resp.GetCreatedAdmins())) + for i, admin := range resp.GetCreatedAdmins() { + users[i] = admin.GetUserId() + } + i.TriggerUserByID(ctx, users...) + return resp } @@ -302,6 +329,7 @@ func (i *Instance) CreateHumanUserVerified(ctx context.Context, org, email, phon }, }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } @@ -313,6 +341,7 @@ func (i *Instance) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserRe AccessTokenType: user_pb.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER, }) logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetUserId()) return resp } @@ -472,6 +501,26 @@ func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string) return resp } +func (i *Instance) AddGenericOIDCProvider(ctx context.Context, name string) *admin.AddGenericOIDCProviderResponse { + resp, err := i.Client.Admin.AddGenericOIDCProvider(ctx, &admin.AddGenericOIDCProviderRequest{ + Name: name, + Issuer: "https://example.com", + ClientId: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid", "profile", "email"}, + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + IsIdTokenMapping: false, + }) + logging.OnError(err).Panic("create generic oidc idp") + return resp +} + func (i *Instance) AddSAMLProvider(ctx context.Context) string { resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{ Name: "saml-idp", @@ -526,101 +575,46 @@ func (i *Instance) AddSAMLPostProvider(ctx context.Context) string { return resp.GetId() } -/* -func (s *Instance) CreateIntent(t *testing.T, ctx context.Context, idpID string) string { - resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user.StartIdentityProviderIntentRequest{ +func (i *Instance) AddLDAPProvider(ctx context.Context) string { + resp, err := i.Client.Admin.AddLDAPProvider(ctx, &admin.AddLDAPProviderRequest{ + Name: "ldap-idp-post", + Servers: []string{"https://localhost:8000"}, + StartTls: false, + BaseDn: "baseDn", + BindDn: "admin", + BindPassword: "admin", + UserBase: "dn", + UserObjectClasses: []string{"user"}, + UserFilters: []string{"(objectclass=*)"}, + Timeout: durationpb.New(10 * time.Second), + Attributes: &idp.LDAPAttributes{ + IdAttribute: "id", + }, + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + logging.OnError(err).Panic("create ldap idp") + return resp.GetId() +} + +func (i *Instance) CreateIntent(ctx context.Context, idpID string) *user_v2.StartIdentityProviderIntentResponse { + resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user_v2.StartIdentityProviderIntentRequest{ IdpId: idpID, - Content: &user.StartIdentityProviderIntentRequest_Urls{ - Urls: &user.RedirectURLs{ + Content: &user_v2.StartIdentityProviderIntentRequest_Urls{ + Urls: &user_v2.RedirectURLs{ SuccessUrl: "https://example.com/success", FailureUrl: "https://example.com/failure", }, - AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, }, }) logging.OnError(err).Fatal("create generic OAuth idp") return resp } -func (i *Instance) CreateIntent(t *testing.T, ctx context.Context, idpID string) string { - ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) - writeModel, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Instance.InstanceID()) - require.NoError(t, err) - return writeModel.AggregateID -} - -func (i *Instance) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { - ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) - intentID := s.CreateIntent(t, ctx, idpID) - writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) - require.NoError(t, err) - idpUser := openid.NewUser( - &oidc.UserInfo{ - Subject: idpUserID, - UserInfoProfile: oidc.UserInfoProfile{ - PreferredUsername: "username", - }, - }, - ) - idpSession := &openid.Session{ - Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ - Token: &oauth2.Token{ - AccessToken: "accessToken", - }, - IDToken: "idToken", - }, - } - token, err := s.Commands.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, userID) - require.NoError(t, err) - return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence -} - -func (s *Instance) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { - ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) - intentID := s.CreateIntent(t, ctx, idpID) - writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) - require.NoError(t, err) - username := "username" - lang := language.Make("en") - idpUser := ldap.NewUser( - idpUserID, - "", - "", - "", - "", - username, - "", - false, - "", - false, - lang, - "", - "", - ) - attributes := map[string][]string{"id": {idpUserID}, "username": {username}, "language": {lang.String()}} - token, err := s.Commands.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, userID, attributes) - require.NoError(t, err) - return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence -} - -func (s *Instance) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { - ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) - intentID := s.CreateIntent(t, ctx, idpID) - writeModel, err := s.Server.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) - require.NoError(t, err) - - idpUser := &saml.UserMapper{ - ID: idpUserID, - Attributes: map[string][]string{"attribute1": {"value1"}}, - } - assertion := &crewjam_saml.Assertion{ID: "id"} - - token, err := s.Server.Commands.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, userID, assertion) - require.NoError(t, err) - return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence -} -*/ - func (i *Instance) CreateVerifiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) { return i.CreateVerifiedWebAuthNSessionWithLifetime(t, ctx, userID, 0) } @@ -727,47 +721,52 @@ func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoin if name == "" { name = gofakeit.Name() } - reqTarget := &action.Target{ + req := &action.CreateTargetRequest{ Name: name, Endpoint: endpoint, - Timeout: durationpb.New(10 * time.Second), + Timeout: durationpb.New(5 * time.Second), } switch ty { case domain.TargetTypeWebhook: - reqTarget.TargetType = &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ + req.TargetType = &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ InterruptOnError: interrupt, }, } case domain.TargetTypeCall: - reqTarget.TargetType = &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ + req.TargetType = &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ InterruptOnError: interrupt, }, } case domain.TargetTypeAsync: - reqTarget.TargetType = &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, + req.TargetType = &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, } } - target, err := i.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget}) + target, err := i.Client.ActionV2beta.CreateTarget(ctx, req) require.NoError(t, err) return target } +func (i *Instance) DeleteTarget(ctx context.Context, t *testing.T, id string) { + _, err := i.Client.ActionV2beta.DeleteTarget(ctx, &action.DeleteTargetRequest{ + Id: id, + }) + require.NoError(t, err) +} + func (i *Instance) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) { - _, err := i.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ + _, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, }) require.NoError(t, err) } -func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { - target, err := i.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ +func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []string) *action.SetExecutionResponse { + target, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, - Execution: &action.Execution{ - Targets: targets, - }, + Targets: targets, }) require.NoError(t, err) return target diff --git a/internal/integration/config.go b/internal/integration/config.go index 5aea740752..0033e00104 100644 --- a/internal/integration/config.go +++ b/internal/integration/config.go @@ -20,10 +20,8 @@ type Config struct { WebAuthNName string } -var ( - //go:embed config/client.yaml - clientYAML []byte -) +//go:embed config/client.yaml +var clientYAML []byte var ( tmpDir string @@ -49,5 +47,6 @@ func init() { if err := loadedConfig.Log.SetLogger(); err != nil { panic(err) } - SystemToken = systemUserToken() + SystemToken = createSystemUserToken() + SystemUserWithNoPermissionsToken = createSystemUserWithNoPermissionsToken() } diff --git a/internal/integration/config/cockroach.yaml b/internal/integration/config/cockroach.yaml deleted file mode 100644 index 920e3cd6ec..0000000000 --- a/internal/integration/config/cockroach.yaml +++ /dev/null @@ -1,10 +0,0 @@ -Database: - cockroach: - Host: localhost - Port: 26257 - Database: zitadel - Options: "" - User: - Username: zitadel - Admin: - Username: root diff --git a/internal/integration/config/docker-compose.yaml b/internal/integration/config/docker-compose.yaml index 19c68ae405..8b54a22aec 100644 --- a/internal/integration/config/docker-compose.yaml +++ b/internal/integration/config/docker-compose.yaml @@ -1,11 +1,6 @@ version: '3.8' services: - cockroach: - extends: - file: '../../../e2e/config/localhost/docker-compose.yaml' - service: 'db' - postgres: restart: 'always' image: 'postgres:latest' diff --git a/internal/integration/config/postgres.yaml b/internal/integration/config/postgres.yaml index df1d08d3bc..904f973d56 100644 --- a/internal/integration/config/postgres.yaml +++ b/internal/integration/config/postgres.yaml @@ -1,16 +1,11 @@ Database: - EventPushConnRatio: 0.2 # 4 - ProjectionSpoolerConnRatio: 0.3 # 6 postgres: - Host: localhost - Port: 5432 - Database: zitadel MaxOpenConns: 20 MaxIdleConns: 20 MaxConnLifetime: 1h MaxConnIdleTime: 5m User: - Username: zitadel + Password: zitadel SSL: Mode: disable Admin: diff --git a/internal/integration/config/system-user-with-no-permissions.pem b/internal/integration/config/system-user-with-no-permissions.pem new file mode 100644 index 0000000000..801944ca75 --- /dev/null +++ b/internal/integration/config/system-user-with-no-permissions.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMxYRfqb4fdnBl +ZmYweqUaZnWQv8RhWDYGifYGen00ozCFT2L6gGov4YCxRVe+l3aFQ79j5SJb1C+v +H68DJkyCTrhDpATqdjVuCu7CEEI//16Ivfmj3gbNdsp0IcDKVIAF0bN9kve5ofRX +CgU6DIx8GjLsXSooSniZnJ4d/Rnt69mpSsPkykUs3RpG2NSOn3WLAoVKh1q/kqeV +qf8eQ+KzuyD/R9QNAPiyB+ivAuOtVuvmIqojQYK5o8veTg/waBxdmzkim7eg8J7B +VDSjBeHagS5K9IJr/Q2VeO0rZOOeJfLlH9xlSrDvc3AIS/3HtkqI268kNkvpGz0I +sg61pUQtAgMBAAECggEAFzZrv1WPaQNAAex6fdR/fKS4Dqwcjxu7XuUpeUSB+GfP +dLAUR2/c8rPJ45FmaGJz9AIpoWiTe5Z33XYJRyjt1U/zQQ4fFGV1JoXtfHkvX3u1 +5DEFZQDT2NYViMRXFNYNvUfow9Rz/nuG/cJEfd+7W6x7SLANJ1MuY1Ao35OQjsOG +ftTtmEUppEIXyWL0PCeHQc83z8aJrP+p4hpjJOW2mui0NR2Hk456DGYXg8I8fcQD +ar7Ar7/A6thR0OmwG7tkkLjRiCjGwnkr19hCNLz+QAWB2o284T12zZueOqRuYQzu +KwNBZKJlClsPkhdZSPLL4RMFP6hJjKoP5mY0Zdzh8QKBgQDEPrM70aZQiweXHqoE +/vZry7tphGycoEAf6nwBBrZaRPpJdnEA61LBlJFv7C3s59uy6L7nHssTyVUJha9i +zFCWRQ0mHNrwxF5Ybd5p//hgblt3X53IV6vZBFF1+OrwRS/AKki3GynDc/oI++hu +PGHWmUF6lIi3uzWwOTqk6EGovQKBgQC3oqpUlpJ78e0zPjIr9ov61TtnPzAa883D +LL7fuNYP9zxIMoFZw++2bZfT5tbINflQdZnVVDNs5KiwtEu3oZJrsqXpQmzCl3j2 +KA9FTdVJQXc2lU90uYb76c5JZPownojbXFFOPQokBqfsYLSdfvNVHSQGjZ3C90wL +YZC0vA9YMQKBgQDCKSraD2YWoEeFO+CJityx8GNfVZbELETljvDbbxGyJDbhwh6y +AyHgxyZR7wHNN+UFkQN31d6kl/jbr/nDrVQ6KN2GjNwNhKu3oBSDGa9bcTRr2h1Y +32z2DTCvoPSJflptLSi+iVB7wd5rTxk7H+DJGt5O8nCGH+JRlX2xNN3pnQKBgDdA +u21eLM8cWNmNQj1WHoInfIsxSQEjEGtEYF4iWE5PfpTelWrz+IF0cjVxBHkTPGPI +LrQwdJS0LEmWxh2HgO3kv+TydpUKTHwMS6P3qlAzYXJL9K9TT1km3UnaFylf2h/e +pBwdY5q5YfdOlam50+9tKDTMkYZjMD9QaODooNlRAoGAOWow99WCATFtRrG+mGyl +UpwApgkZKT0nhkXUnLdNoQVeP0WHeQBSoOA24YnGBntvG/98Uj2rOwdCAYzTGepz +91bNqscrSOPdD3VN85GEl2DQKtxsRCKCdPKmYkvC/WMGhuzXSIp2U+ePgqEjEQO2 +Sn4xXZ1zwl+4cYHmDvzEQnA= +-----END PRIVATE KEY----- diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index e2642d9b8f..bb8d86376d 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -82,6 +82,13 @@ SystemAPIUsers: - "ORG_OWNER" - cypress: KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + - system-user-with-no-permissions: + KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFqTVdFWDZtK0gzWndaV1ptTUhxbApHbVoxa0wvRVlWZzJCb24yQm5wOU5LTXdoVTlpK29CcUwrR0FzVVZYdnBkMmhVTy9ZK1VpVzlRdnJ4K3ZBeVpNCmdrNjRRNlFFNm5ZMWJncnV3aEJDUC85ZWlMMzVvOTRHelhiS2RDSEF5bFNBQmRHemZaTDN1YUgwVndvRk9neU0KZkJveTdGMHFLRXA0bVp5ZUhmMFo3ZXZacVVyRDVNcEZMTjBhUnRqVWpwOTFpd0tGU29kYXY1S25sYW4vSGtQaQpzN3NnLzBmVURRRDRzZ2ZvcndManJWYnI1aUtxSTBHQ3VhUEwzazRQOEdnY1haczVJcHUzb1BDZXdWUTBvd1hoCjJvRXVTdlNDYS8wTmxYanRLMlRqbmlYeTVSL2NaVXF3NzNOd0NFdjl4N1pLaU51dkpEWkw2UnM5Q0xJT3RhVkUKTFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + Memberships: + # MemberType System allows the user to access all APIs for all instances or organizations + - MemberType: IAM + Roles: + - "NO_ROLES" InitProjections: Enabled: true diff --git a/internal/integration/instance.go b/internal/integration/instance.go index 6113bf0e37..66e2cf18ec 100644 --- a/internal/integration/instance.go +++ b/internal/integration/instance.go @@ -97,22 +97,6 @@ type Instance struct { WebAuthN *webauthn.Client } -// GetFirstInstance returns the default instance and org information, -// with authorized machine users. -// Using the first instance is not recommended as parallel test might -// interfere with each other. -// It is recommended to use [NewInstance] instead. -func GetFirstInstance(ctx context.Context) *Instance { - i := &Instance{ - Config: loadedConfig, - Domain: loadedConfig.Hostname, - } - token := loadInstanceOwnerPAT() - i.setClient(ctx) - i.setupInstance(ctx, token) - return i -} - // NewInstance returns a new instance that can be used for integration tests. // The instance contains a gRPC client connected to the domain of this instance. // The included users are the IAM_OWNER, ORG_OWNER of the default org and diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 4d7f5277c9..159fcb0119 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -438,7 +438,7 @@ func (i *Instance) CreateOIDCCredentialsClientInactive(ctx context.Context) (mac return machine, name, secret.GetClientId(), secret.GetClientSecret(), nil } -func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) { +func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context, keyLifetime time.Duration) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) { name = gofakeit.Username() machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ Name: name, @@ -451,7 +451,7 @@ func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *man keyResp, err := i.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{ UserId: machine.GetUserId(), Type: authn.KeyType_KEY_TYPE_JSON, - ExpirationDate: timestamppb.New(time.Now().Add(time.Hour)), + ExpirationDate: timestamppb.New(time.Now().Add(keyLifetime)), }) if err != nil { return nil, "", nil, err @@ -466,3 +466,11 @@ func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *man return machine, name, keyResp.GetKeyDetails(), nil } + +func (i *Instance) CreateDeviceAuthorizationRequest(ctx context.Context, clientID string, scopes ...string) (*oidc.DeviceAuthorizationResponse, error) { + provider, err := i.CreateRelyingParty(ctx, clientID, "", scopes...) + if err != nil { + return nil, err + } + return rp.DeviceAuthorization(ctx, scopes, provider, nil) +} diff --git a/internal/integration/saml.go b/internal/integration/saml.go index 483543b322..28934ac421 100644 --- a/internal/integration/saml.go +++ b/internal/integration/saml.go @@ -17,9 +17,11 @@ import ( "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" "github.com/zitadel/logging" + "github.com/zitadel/saml/pkg/provider" http_util "github.com/zitadel/zitadel/internal/api/http" oidc_internal "github.com/zitadel/zitadel/internal/api/oidc" + app_pb "github.com/zitadel/zitadel/pkg/grpc/app" "github.com/zitadel/zitadel/pkg/grpc/management" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" session_pb "github.com/zitadel/zitadel/pkg/grpc/session/v2" @@ -102,7 +104,7 @@ func CreateSAMLSP(root string, idpMetadata *saml.EntityDescriptor, binding strin return sp, nil } -func (i *Instance) CreateSAMLClient(ctx context.Context, projectID string, m *samlsp.Middleware) (*management.AddSAMLAppResponse, error) { +func (i *Instance) CreateSAMLClientLoginVersion(ctx context.Context, projectID string, m *samlsp.Middleware, loginVersion *app_pb.LoginVersion) (*management.AddSAMLAppResponse, error) { spMetadata, err := xml.MarshalIndent(m.ServiceProvider.Metadata(), "", " ") if err != nil { return nil, err @@ -114,9 +116,10 @@ func (i *Instance) CreateSAMLClient(ctx context.Context, projectID string, m *sa } resp, err := i.Client.Mgmt.AddSAMLApp(ctx, &management.AddSAMLAppRequest{ - ProjectId: projectID, - Name: fmt.Sprintf("app-%s", gofakeit.AppName()), - Metadata: &management.AddSAMLAppRequest_MetadataXml{MetadataXml: spMetadata}, + ProjectId: projectID, + Name: fmt.Sprintf("app-%s", gofakeit.AppName()), + Metadata: &management.AddSAMLAppRequest_MetadataXml{MetadataXml: spMetadata}, + LoginVersion: loginVersion, }) if err != nil { return nil, err @@ -136,7 +139,19 @@ func (i *Instance) CreateSAMLClient(ctx context.Context, projectID string, m *sa }) } -func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient string, acs saml.Endpoint, relayState string, responseBinding string) (now time.Time, authRequestID string, err error) { +func (i *Instance) CreateSAMLClient(ctx context.Context, projectID string, m *samlsp.Middleware) (*management.AddSAMLAppResponse, error) { + return i.CreateSAMLClientLoginVersion(ctx, projectID, m, nil) +} + +func (i *Instance) CreateSAMLAuthRequestWithoutLoginClientHeader(m *samlsp.Middleware, loginBaseURI string, acs saml.Endpoint, relayState, responseBinding string) (now time.Time, authRequestID string, err error) { + return i.createSAMLAuthRequest(m, "", loginBaseURI, acs, relayState, responseBinding) +} + +func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient string, acs saml.Endpoint, relayState, responseBinding string) (now time.Time, authRequestID string, err error) { + return i.createSAMLAuthRequest(m, loginClient, "", acs, relayState, responseBinding) +} + +func (i *Instance) createSAMLAuthRequest(m *samlsp.Middleware, loginClient, loginBaseURI string, acs saml.Endpoint, relayState, responseBinding string) (now time.Time, authRequestID string, err error) { authReq, err := m.ServiceProvider.MakeAuthenticationRequest(acs.Location, acs.Binding, responseBinding) if err != nil { return now, "", err @@ -147,7 +162,11 @@ func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient strin return now, "", err } - req, err := GetRequest(redirectURL.String(), map[string]string{oidc_internal.LoginClientHeader: loginClient}) + var headers map[string]string + if loginClient != "" { + headers = map[string]string{oidc_internal.LoginClientHeader: loginClient} + } + req, err := GetRequest(redirectURL.String(), headers) if err != nil { return now, "", fmt.Errorf("get request: %w", err) } @@ -158,11 +177,13 @@ func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient strin return now, "", fmt.Errorf("check redirect: %w", err) } - prefixWithHost := i.Issuer() + i.Config.LoginURLV2 - if !strings.HasPrefix(loc.String(), prefixWithHost) { - return now, "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String()) + if loginBaseURI == "" { + loginBaseURI = i.Issuer() + i.Config.LoginURLV2 } - return now, strings.TrimPrefix(loc.String(), prefixWithHost), nil + if !strings.HasPrefix(loc.String(), loginBaseURI) { + return now, "", fmt.Errorf("login location has not prefix %s, but is %s", loginBaseURI, loc.String()) + } + return now, strings.TrimPrefix(loc.String(), loginBaseURI), nil } func (i *Instance) FailSAMLAuthRequest(ctx context.Context, id string, reason saml_pb.ErrorReason) *saml_pb.CreateResponseResponse { @@ -200,8 +221,15 @@ func (i *Instance) SuccessfulSAMLAuthRequest(ctx context.Context, userId, id str } func (i *Instance) GetSAMLIDPMetadata() (*saml.EntityDescriptor, error) { - idpEntityID := http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure) + "/saml/v2/metadata" - resp, err := http.Get(idpEntityID) + issuer := i.Issuer() + "/saml/v2" + idpEntityID := issuer + "/metadata" + + req, err := http.NewRequestWithContext(provider.ContextWithIssuer(context.Background(), issuer), http.MethodGet, idpEntityID, nil) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go index 959353ae5f..633ebf424f 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -3,6 +3,9 @@ package sink import ( + "bytes" + "context" + "encoding/json" "errors" "io" "net/http" @@ -10,11 +13,23 @@ import ( "path" "sync" "sync/atomic" + "time" + crewjam_saml "github.com/crewjam/saml" "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" "github.com/sirupsen/logrus" "github.com/zitadel/logging" + "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/oauth2" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" + "github.com/zitadel/zitadel/internal/idp/providers/oauth" + openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" ) const ( @@ -33,6 +48,78 @@ func CallURL(ch Channel) string { return u.String() } +func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentOAuthPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + }) + if err != nil { + return "", "", time.Time{}, uint64(0), err + } + return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil +} + +func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentOIDCPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + }) + if err != nil { + return "", "", time.Time{}, uint64(0), err + } + return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil +} + +func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentSAMLPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + }) + if err != nil { + return "", "", time.Time{}, uint64(0), err + } + return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil +} + +func SuccessfulLDAPIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentLDAPPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + }) + if err != nil { + return "", "", time.Time{}, uint64(0), err + } + return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil +} + // StartServer starts a simple HTTP server on localhost:8081 // ZITADEL can use the server to send HTTP requests which can be // used to validate tests through [Subscribe]rs. @@ -41,7 +128,7 @@ func CallURL(ch Channel) string { // [CallURL] can be used to obtain the full URL for a given Channel. // // This function is only active when the `integration` build tag is enabled -func StartServer() (close func()) { +func StartServer(commands *command.Commands) (close func()) { router := chi.NewRouter() for _, ch := range ChannelValues() { fwd := &forwarder{ @@ -50,6 +137,10 @@ func StartServer() (close func()) { } router.HandleFunc(rootPath(ch), fwd.receiveHandler) router.HandleFunc(subscribePath(ch), fwd.subscriptionHandler) + router.HandleFunc(successfulIntentOAuthPath(), successfulIntentHandler(commands, createSuccessfulOAuthIntent)) + router.HandleFunc(successfulIntentOIDCPath(), successfulIntentHandler(commands, createSuccessfulOIDCIntent)) + router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent)) + router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent)) } s := &http.Server{ Addr: listenAddr, @@ -76,6 +167,30 @@ func subscribePath(c Channel) string { return path.Join("/", c.String(), "subscribe") } +func intentPath() string { + return path.Join("/", "intent") +} + +func successfulIntentPath() string { + return path.Join(intentPath(), "/", "successful") +} + +func successfulIntentOAuthPath() string { + return path.Join(successfulIntentPath(), "/", "oauth") +} + +func successfulIntentOIDCPath() string { + return path.Join(successfulIntentPath(), "/", "oidc") +} + +func successfulIntentSAMLPath() string { + return path.Join(successfulIntentPath(), "/", "saml") +} + +func successfulIntentLDAPPath() string { + return path.Join(successfulIntentPath(), "/", "ldap") +} + // forwarder handles incoming HTTP requests from ZITADEL and // forwards them to all subscribed web sockets. type forwarder struct { @@ -165,3 +280,200 @@ func readLoop(ws *websocket.Conn) (done chan error) { return done } + +type SuccessfulIntentRequest struct { + InstanceID string `json:"instance_id"` + IDPID string `json:"idp_id"` + IDPUserID string `json:"idp_user_id"` + UserID string `json:"user_id"` +} +type SuccessfulIntentResponse struct { + IntentID string `json:"intent_id"` + Token string `json:"token"` + ChangeDate time.Time `json:"change_date"` + Sequence uint64 `json:"sequence"` +} + +func callIntent(url string, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + data, err := json.Marshal(req) + if err != nil { + return nil, err + } + + resp, err := http.Post(url, "application/json", io.NopCloser(bytes.NewReader(data))) + if err != nil { + return nil, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(string(body)) + } + result := new(SuccessfulIntentResponse) + if err := json.Unmarshal(body, result); err != nil { + return nil, err + } + return result, nil +} + +func successfulIntentHandler(cmd *command.Commands, createIntent func(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + req := &SuccessfulIntentRequest{} + if err := json.Unmarshal(body, req); err != nil { + } + + ctx := authz.WithInstanceID(r.Context(), req.InstanceID) + resp, err := createIntent(ctx, cmd, req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + data, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Write(data) + return + } +} + +func createIntent(ctx context.Context, cmd *command.Commands, instanceID, idpID string) (string, error) { + writeModel, _, err := cmd.CreateIntent(ctx, "", idpID, "https://example.com/success", "https://example.com/failure", instanceID, nil) + if err != nil { + return "", err + } + return writeModel.AggregateID, nil +} + +func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) + if err != nil { + return nil, err + } + writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) + if err != nil { + return nil, err + } + idAttribute := "id" + idpUser := oauth.NewUserMapper(idAttribute) + idpUser.RawInfo = map[string]interface{}{ + idAttribute: req.IDPUserID, + "preferred_username": "username", + } + idpSession := &oauth.Session{ + Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ + Token: &oauth2.Token{ + AccessToken: "accessToken", + }, + IDToken: "idToken", + }, + } + token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, req.UserID) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} + +func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) + writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) + idpUser := openid.NewUser( + &oidc.UserInfo{ + Subject: req.IDPUserID, + UserInfoProfile: oidc.UserInfoProfile{ + PreferredUsername: "username", + }, + }, + ) + idpSession := &openid.Session{ + Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ + Token: &oauth2.Token{ + AccessToken: "accessToken", + }, + IDToken: "idToken", + }, + } + token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, req.UserID) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} + +func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) + writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) + + idpUser := &saml.UserMapper{ + ID: req.IDPUserID, + Attributes: map[string][]string{"attribute1": {"value1"}}, + } + assertion := &crewjam_saml.Assertion{ID: "id"} + + token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, assertion) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} + +func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) + writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) + username := "username" + lang := language.Make("en") + idpUser := ldap.NewUser( + req.IDPUserID, + "", + "", + "", + "", + username, + "", + false, + "", + false, + lang, + "", + "", + ) + attributes := map[string][]string{"id": {req.IDPUserID}, "username": {username}, "language": {lang.String()}} + token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, attributes) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} diff --git a/internal/integration/sink/stub.go b/internal/integration/sink/stub.go index 01d1047f34..62e1d541e1 100644 --- a/internal/integration/sink/stub.go +++ b/internal/integration/sink/stub.go @@ -2,8 +2,10 @@ package sink +import "github.com/zitadel/zitadel/internal/command" + // StartServer and its returned close function are a no-op // when the `integration` build tag is disabled. -func StartServer() (close func()) { +func StartServer(cmd *command.Commands) (close func()) { return func() {} } diff --git a/internal/integration/system.go b/internal/integration/system.go index a9673a40ae..badc3db355 100644 --- a/internal/integration/system.go +++ b/internal/integration/system.go @@ -17,13 +17,16 @@ import ( var ( //go:embed config/system-user-key.pem systemUserKey []byte + //go:embed config/system-user-with-no-permissions.pem + systemUserWithNoPermissions []byte ) var ( // SystemClient creates a system connection once and reuses it on every use. // Each client call automatically gets the authorization context for the system user. - SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient) - SystemToken string + SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient) + SystemToken string + SystemUserWithNoPermissionsToken string ) func systemClient() system.SystemServiceClient { @@ -40,7 +43,7 @@ func systemClient() system.SystemServiceClient { return system.NewSystemServiceClient(cc) } -func systemUserToken() string { +func createSystemUserToken() string { const ISSUER = "tester" audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure) signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "") @@ -54,6 +57,24 @@ func systemUserToken() string { return token } +func createSystemUserWithNoPermissionsToken() string { + const ISSUER = "system-user-with-no-permissions" + audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure) + signer, err := client.NewSignerFromPrivateKeyByte(systemUserWithNoPermissions, "") + if err != nil { + panic(err) + } + token, err := client.SignedJWTProfileAssertion(ISSUER, []string{audience}, time.Hour, signer) + if err != nil { + panic(err) + } + return token +} + func WithSystemAuthorization(ctx context.Context) context.Context { return WithAuthorizationToken(ctx, SystemToken) } + +func WithSystemUserWithNoPermissionsAuthorization(ctx context.Context) context.Context { + return WithAuthorizationToken(ctx, SystemUserWithNoPermissionsToken) +} diff --git a/internal/notification/channels/channel.go b/internal/notification/channels/channel.go index 5f4f8f4d88..d491ae894d 100644 --- a/internal/notification/channels/channel.go +++ b/internal/notification/channels/channel.go @@ -3,7 +3,7 @@ package channels import "github.com/zitadel/zitadel/internal/eventstore" type Message interface { - GetTriggeringEvent() eventstore.Event + GetTriggeringEventType() eventstore.EventType GetContent() (string, error) } diff --git a/internal/notification/channels/instrumenting/logging.go b/internal/notification/channels/instrumenting/logging.go index 6904f7c263..421ed1bbfb 100644 --- a/internal/notification/channels/instrumenting/logging.go +++ b/internal/notification/channels/instrumenting/logging.go @@ -13,7 +13,7 @@ func logMessages(ctx context.Context, channel channels.NotificationChannel) chan return channels.HandleMessageFunc(func(message channels.Message) error { logEntry := logging.WithFields( "instance", authz.GetInstance(ctx).InstanceID(), - "triggering_event_type", message.GetTriggeringEvent().Type(), + "triggering_event_type", message.GetTriggeringEventType(), ) logEntry.Debug("sending notification") err := channel.HandleMessage(message) diff --git a/internal/notification/channels/instrumenting/metrics.go b/internal/notification/channels/instrumenting/metrics.go index 09a402e63a..7033fbab51 100644 --- a/internal/notification/channels/instrumenting/metrics.go +++ b/internal/notification/channels/instrumenting/metrics.go @@ -24,7 +24,7 @@ func countMessages(ctx context.Context, channel channels.NotificationChannel, su func addCount(ctx context.Context, metricName string, message channels.Message) { labels := map[string]attribute.Value{ - "triggering_event_type": attribute.StringValue(string(message.GetTriggeringEvent().Type())), + "triggering_event_type": attribute.StringValue(string(message.GetTriggeringEventType())), } addCountErr := metrics.AddCount(ctx, metricName, 1, labels) logging.WithFields("name", metricName, "labels", labels).OnError(addCountErr).Error("incrementing counter metric failed") diff --git a/internal/notification/channels/twilio/channel.go b/internal/notification/channels/twilio/channel.go index e13f45e00b..d6024b63dc 100644 --- a/internal/notification/channels/twilio/channel.go +++ b/internal/notification/channels/twilio/channel.go @@ -9,7 +9,6 @@ import ( verify "github.com/twilio/twilio-go/rest/verify/v2" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/channels" "github.com/zitadel/zitadel/internal/notification/messages" "github.com/zitadel/zitadel/internal/zerrors" @@ -39,15 +38,14 @@ func InitChannel(config Config) channels.NotificationChannel { // as it would be a waste of resources and could potentially result in a rate limit. var twilioErr *twilioClient.TwilioRestError if errors.As(err, &twilioErr) && twilioErr.Status >= 400 && twilioErr.Status < 500 { - userID, notificationID := userAndNotificationIDsFromEvent(twilioMsg.TriggeringEvent) logging.WithFields( "error", twilioErr.Message, "status", twilioErr.Status, "code", twilioErr.Code, - "instanceID", twilioMsg.TriggeringEvent.Aggregate().InstanceID, - "userID", userID, - "notificationID", notificationID). - Warn("twilio create verification error") + "instanceID", twilioMsg.InstanceID, + "jobID", twilioMsg.JobID, + "userID", twilioMsg.UserID, + ).Warn("twilio create verification error") return channels.NewCancelError(twilioErr) } @@ -76,24 +74,3 @@ func InitChannel(config Config) channels.NotificationChannel { return nil }) } - -func userAndNotificationIDsFromEvent(event eventstore.Event) (userID, notificationID string) { - aggID := event.Aggregate().ID - - // we cannot cast to the actual event type because of circular dependencies - // so we just check the type... - if event.Aggregate().Type != aggregateTypeNotification { - // in case it's not a notification event, we can directly return the aggregate ID (as it's a user event) - return aggID, "" - } - // ...and unmarshal the event data from the notification event into a struct that contains the fields we need - var data struct { - Request struct { - UserID string `json:"userID"` - } `json:"request"` - } - if err := event.Unmarshal(&data); err != nil { - return "", aggID - } - return data.Request.UserID, aggID -} diff --git a/internal/notification/handlers/back_channel_logout.go b/internal/notification/handlers/back_channel_logout.go index 43d98ada11..f1a99146ca 100644 --- a/internal/notification/handlers/back_channel_logout.go +++ b/internal/notification/handlers/back_channel_logout.go @@ -191,7 +191,7 @@ func (u *backChannelLogoutNotifier) sendLogoutToken(ctx context.Context, oidcSes if err != nil { return err } - err = types.SendSecurityTokenEvent(ctx, set.Config{CallURL: oidcSession.BackChannelLogoutURI}, u.channels, &LogoutTokenMessage{LogoutToken: token}, e).WithoutTemplate() + err = types.SendSecurityTokenEvent(ctx, set.Config{CallURL: oidcSession.BackChannelLogoutURI}, u.channels, &LogoutTokenMessage{LogoutToken: token}, e.Type()).WithoutTemplate() if err != nil { return err } @@ -247,7 +247,7 @@ func (b *backChannelLogoutSession) AppendEvents(events ...eventstore.Event) { BackChannelLogoutURI: e.BackChannelLogoutURI, }) case *sessionlogout.BackChannelLogoutSentEvent: - slices.DeleteFunc(b.sessions, func(session backChannelLogoutOIDCSessions) bool { + b.sessions = slices.DeleteFunc(b.sessions, func(session backChannelLogoutOIDCSessions) bool { return session.OIDCSessionID == e.OIDCSessionID }) } diff --git a/internal/notification/handlers/commands.go b/internal/notification/handlers/commands.go index 90b66bdf48..07969a6bba 100644 --- a/internal/notification/handlers/commands.go +++ b/internal/notification/handlers/commands.go @@ -2,19 +2,13 @@ package handlers import ( "context" - "database/sql" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/milestone" "github.com/zitadel/zitadel/internal/repository/quota" ) type Commands interface { - RequestNotification(ctx context.Context, instanceID string, request *command.NotificationRequest) error - NotificationCanceled(ctx context.Context, tx *sql.Tx, id, resourceOwner string, err error) error - NotificationRetryRequested(ctx context.Context, tx *sql.Tx, id, resourceOwner string, request *command.NotificationRetryRequest, err error) error - NotificationSent(ctx context.Context, tx *sql.Tx, id, instanceID string) error HumanInitCodeSent(ctx context.Context, orgID, userID string) error HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) error PasswordCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error diff --git a/internal/notification/handlers/ctx.go b/internal/notification/handlers/ctx.go index 8f499814aa..b091a61cdd 100644 --- a/internal/notification/handlers/ctx.go +++ b/internal/notification/handlers/ctx.go @@ -15,7 +15,7 @@ func HandlerContext(event *eventstore.Aggregate) context.Context { } func ContextWithNotifier(ctx context.Context, aggregate *eventstore.Aggregate) context.Context { - return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: aggregate.ResourceOwner}) + return authz.WithInstanceID(authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: aggregate.ResourceOwner}), aggregate.InstanceID) } func (n *NotificationQueries) HandlerContext(event *eventstore.Aggregate) (context.Context, error) { diff --git a/internal/notification/handlers/gen_mock.go b/internal/notification/handlers/gen_mock.go index e248633361..5732e29e2f 100644 --- a/internal/notification/handlers/gen_mock.go +++ b/internal/notification/handlers/gen_mock.go @@ -2,3 +2,4 @@ package handlers //go:generate mockgen -package mock -destination ./mock/queries.mock.go github.com/zitadel/zitadel/internal/notification/handlers Queries //go:generate mockgen -package mock -destination ./mock/commands.mock.go github.com/zitadel/zitadel/internal/notification/handlers Commands +//go:generate mockgen -package mock -destination ./mock/queue.mock.go github.com/zitadel/zitadel/internal/notification/handlers Queue diff --git a/internal/notification/handlers/mock/commands.mock.go b/internal/notification/handlers/mock/commands.mock.go index de32ce067c..7d41c30f30 100644 --- a/internal/notification/handlers/mock/commands.mock.go +++ b/internal/notification/handlers/mock/commands.mock.go @@ -11,10 +11,8 @@ package mock import ( context "context" - sql "database/sql" reflect "reflect" - command "github.com/zitadel/zitadel/internal/command" senders "github.com/zitadel/zitadel/internal/notification/senders" milestone "github.com/zitadel/zitadel/internal/repository/milestone" quota "github.com/zitadel/zitadel/internal/repository/quota" @@ -156,48 +154,6 @@ func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), arg0, arg1, arg2, arg3) } -// NotificationCanceled mocks base method. -func (m *MockCommands) NotificationCanceled(arg0 context.Context, arg1 *sql.Tx, arg2, arg3 string, arg4 error) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NotificationCanceled", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// NotificationCanceled indicates an expected call of NotificationCanceled. -func (mr *MockCommandsMockRecorder) NotificationCanceled(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationCanceled", reflect.TypeOf((*MockCommands)(nil).NotificationCanceled), arg0, arg1, arg2, arg3, arg4) -} - -// NotificationRetryRequested mocks base method. -func (m *MockCommands) NotificationRetryRequested(arg0 context.Context, arg1 *sql.Tx, arg2, arg3 string, arg4 *command.NotificationRetryRequest, arg5 error) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NotificationRetryRequested", arg0, arg1, arg2, arg3, arg4, arg5) - ret0, _ := ret[0].(error) - return ret0 -} - -// NotificationRetryRequested indicates an expected call of NotificationRetryRequested. -func (mr *MockCommandsMockRecorder) NotificationRetryRequested(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationRetryRequested", reflect.TypeOf((*MockCommands)(nil).NotificationRetryRequested), arg0, arg1, arg2, arg3, arg4, arg5) -} - -// NotificationSent mocks base method. -func (m *MockCommands) NotificationSent(arg0 context.Context, arg1 *sql.Tx, arg2, arg3 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NotificationSent", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// NotificationSent indicates an expected call of NotificationSent. -func (mr *MockCommandsMockRecorder) NotificationSent(arg0, arg1, arg2, arg3 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationSent", reflect.TypeOf((*MockCommands)(nil).NotificationSent), arg0, arg1, arg2, arg3) -} - // OTPEmailSent mocks base method. func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() @@ -254,20 +210,6 @@ func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2, arg3 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), arg0, arg1, arg2, arg3) } -// RequestNotification mocks base method. -func (m *MockCommands) RequestNotification(arg0 context.Context, arg1 string, arg2 *command.NotificationRequest) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestNotification", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// RequestNotification indicates an expected call of RequestNotification. -func (mr *MockCommandsMockRecorder) RequestNotification(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestNotification", reflect.TypeOf((*MockCommands)(nil).RequestNotification), arg0, arg1, arg2) -} - // UsageNotificationSent mocks base method. func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.NotificationDueEvent) error { m.ctrl.T.Helper() diff --git a/internal/notification/handlers/mock/queue.mock.go b/internal/notification/handlers/mock/queue.mock.go new file mode 100644 index 0000000000..e1387595db --- /dev/null +++ b/internal/notification/handlers/mock/queue.mock.go @@ -0,0 +1,61 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/notification/handlers (interfaces: Queue) +// +// Generated by this command: +// +// mockgen -package mock -destination ./mock/queue.mock.go github.com/zitadel/zitadel/internal/notification/handlers Queue +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + river "github.com/riverqueue/river" + queue "github.com/zitadel/zitadel/internal/queue" + gomock "go.uber.org/mock/gomock" +) + +// MockQueue is a mock of Queue interface. +type MockQueue struct { + ctrl *gomock.Controller + recorder *MockQueueMockRecorder +} + +// MockQueueMockRecorder is the mock recorder for MockQueue. +type MockQueueMockRecorder struct { + mock *MockQueue +} + +// NewMockQueue creates a new mock instance. +func NewMockQueue(ctrl *gomock.Controller) *MockQueue { + mock := &MockQueue{ctrl: ctrl} + mock.recorder = &MockQueueMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockQueue) EXPECT() *MockQueueMockRecorder { + return m.recorder +} + +// Insert mocks base method. +func (m *MockQueue) Insert(arg0 context.Context, arg1 river.JobArgs, arg2 ...queue.InsertOpt) error { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Insert", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockQueueMockRecorder) Insert(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...) +} diff --git a/internal/notification/handlers/notification_worker.go b/internal/notification/handlers/notification_worker.go index 9c00552acb..fa082bc345 100644 --- a/internal/notification/handlers/notification_worker.go +++ b/internal/notification/handlers/notification_worker.go @@ -2,27 +2,23 @@ package handlers import ( "context" - "database/sql" "errors" - "math/rand/v2" - "slices" + "fmt" + "strconv" "strings" "time" + "github.com/riverqueue/river" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/channels" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/types" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/notification" ) @@ -32,29 +28,62 @@ const ( ) type NotificationWorker struct { + river.WorkerDefaults[*notification.Request] + commands Commands queries *NotificationQueries - es *eventstore.Eventstore - client *database.DB channels types.ChannelChains config WorkerConfig now nowFunc - backOff func(current time.Duration) time.Duration +} + +// Timeout implements the Timeout-function of [river.Worker]. +// Maximum time a job can run before the context gets cancelled. +func (w *NotificationWorker) Timeout(*river.Job[*notification.Request]) time.Duration { + return w.config.TransactionDuration +} + +// Work implements [river.Worker]. +func (w *NotificationWorker) Work(ctx context.Context, job *river.Job[*notification.Request]) error { + ctx = ContextWithNotifier(ctx, job.Args.Aggregate) + + // if the notification is too old, we can directly cancel + if job.CreatedAt.Add(w.config.MaxTtl).Before(w.now()) { + return river.JobCancel(errors.New("notification is too old")) + } + + // We do not trigger the projection to reduce load on the database. By the time the notification is processed, + // the user should be projected anyway. If not, it will just wait for the next run. + // We are aware that the user can change during the time the notification is in the queue. + notifyUser, err := w.queries.GetNotifyUserByID(ctx, false, job.Args.UserID) + if err != nil { + return err + } + + // The domain claimed event requires the domain as argument, but lacks the user when creating the request event. + // Since we set it into the request arguments, it will be passed into a potential retry event. + if job.Args.RequiresPreviousDomain && job.Args.Args != nil && job.Args.Args.Domain == "" { + index := strings.LastIndex(notifyUser.LastEmail, "@") + job.Args.Args.Domain = notifyUser.LastEmail[index+1:] + } + + err = w.sendNotificationQueue(ctx, job.Args, strconv.Itoa(int(job.ID)), notifyUser) + if err == nil { + return nil + } + // if the error explicitly specifies, we cancel the notification + if errors.Is(err, &channels.CancelError{}) { + return river.JobCancel(err) + } + return err } type WorkerConfig struct { LegacyEnabled bool Workers uint8 - BulkLimit uint16 - RequeueEvery time.Duration - RetryWorkers uint8 - RetryRequeueEvery time.Duration TransactionDuration time.Duration - MaxAttempts uint8 MaxTtl time.Duration - MinRetryDelay time.Duration - MaxRetryDelay time.Duration - RetryDelayFactor float32 + MaxAttempts uint8 } // nowFunc makes [time.Now] mockable @@ -75,119 +104,36 @@ func NewNotificationWorker( config WorkerConfig, commands Commands, queries *NotificationQueries, - es *eventstore.Eventstore, - client *database.DB, channels types.ChannelChains, ) *NotificationWorker { - // make sure the delay does not get less - if config.RetryDelayFactor < 1 { - config.RetryDelayFactor = 1 - } - w := &NotificationWorker{ + return &NotificationWorker{ config: config, commands: commands, queries: queries, - es: es, - client: client, channels: channels, now: time.Now, } - w.backOff = w.exponentialBackOff - return w } -func (w *NotificationWorker) Start(ctx context.Context) { - if w.config.LegacyEnabled { - return - } - for i := 0; i < int(w.config.Workers); i++ { - go w.schedule(ctx, i, false) - } - for i := 0; i < int(w.config.RetryWorkers); i++ { - go w.schedule(ctx, i, true) +var _ river.Worker[*notification.Request] = (*NotificationWorker)(nil) + +func (w *NotificationWorker) Register(workers *river.Workers, queues map[string]river.QueueConfig) { + river.AddWorker(workers, w) + queues[notification.QueueName] = river.QueueConfig{ + MaxWorkers: int(w.config.Workers), } } -func (w *NotificationWorker) reduceNotificationRequested(ctx, txCtx context.Context, tx *sql.Tx, event *notification.RequestedEvent) (err error) { - ctx = ContextWithNotifier(ctx, event.Aggregate()) - - // if the notification is too old, we can directly cancel - if event.CreatedAt().Add(w.config.MaxTtl).Before(w.now()) { - return w.commands.NotificationCanceled(txCtx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, nil) - } - - // Get the notify user first, so if anything fails afterward we have the current state of the user - // and can pass that to the retry request. - // We do not trigger the projection to reduce load on the database. By the time the notification is processed, - // the user should be projected anyway. If not, it will just wait for the next run. - notifyUser, err := w.queries.GetNotifyUserByID(ctx, false, event.UserID) - if err != nil { - return err - } - - // The domain claimed event requires the domain as argument, but lacks the user when creating the request event. - // Since we set it into the request arguments, it will be passed into a potential retry event. - if event.RequiresPreviousDomain && event.Request.Args != nil && event.Request.Args.Domain == "" { - index := strings.LastIndex(notifyUser.LastEmail, "@") - event.Request.Args.Domain = notifyUser.LastEmail[index+1:] - } - - err = w.sendNotification(ctx, txCtx, tx, event.Request, notifyUser, event) - if err == nil { - return nil - } - // if retries are disabled or if the error explicitly specifies, we cancel the notification - if w.config.MaxAttempts <= 1 || errors.Is(err, &channels.CancelError{}) { - return w.commands.NotificationCanceled(txCtx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, err) - } - // otherwise we retry after a backoff delay - return w.commands.NotificationRetryRequested( - txCtx, - tx, - event.Aggregate().ID, - event.Aggregate().ResourceOwner, - notificationEventToRequest(event.Request, notifyUser, w.backOff(0)), - err, - ) -} - -func (w *NotificationWorker) reduceNotificationRetry(ctx, txCtx context.Context, tx *sql.Tx, event *notification.RetryRequestedEvent) (err error) { - ctx = ContextWithNotifier(ctx, event.Aggregate()) - - // if the notification is too old, we can directly cancel - if event.CreatedAt().Add(w.config.MaxTtl).Before(w.now()) { - return w.commands.NotificationCanceled(txCtx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, err) - } - - if event.CreatedAt().Add(event.BackOff).After(w.now()) { - return nil - } - err = w.sendNotification(ctx, txCtx, tx, event.Request, event.NotifyUser, event) - if err == nil { - return nil - } - // if the max attempts are reached or if the error explicitly specifies, we cancel the notification - if event.Sequence() >= uint64(w.config.MaxAttempts) || errors.Is(err, &channels.CancelError{}) { - return w.commands.NotificationCanceled(txCtx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, err) - } - // otherwise we retry after a backoff delay - return w.commands.NotificationRetryRequested(txCtx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, notificationEventToRequest( - event.Request, - event.NotifyUser, - w.backOff(event.BackOff), - ), err) -} - -func (w *NotificationWorker) sendNotification(ctx, txCtx context.Context, tx *sql.Tx, request notification.Request, notifyUser *query.NotifyUser, e eventstore.Event) error { - ctx, err := enrichCtx(ctx, request.TriggeredAtOrigin) - if err != nil { - return channels.NewCancelError(err) - } - +func (w *NotificationWorker) sendNotificationQueue(ctx context.Context, request *notification.Request, jobID string, notifyUser *query.NotifyUser) error { // check early that a "sent" handler exists, otherwise we can cancel early sentHandler, ok := sentHandlers[request.EventType] if !ok { logging.Errorf(`no "sent" handler registered for %s`, request.EventType) + return channels.NewCancelError(fmt.Errorf("no sent handler registered for %s", request.EventType)) + } + + ctx, err := enrichCtx(ctx, request.TriggeredAtOrigin) + if err != nil { return channels.NewCancelError(err) } @@ -217,9 +163,9 @@ func (w *NotificationWorker) sendNotification(ctx, txCtx context.Context, tx *sq if err != nil { return err } - notify = types.SendEmail(ctx, w.channels, string(template.Template), translator, notifyUser, colors, e) + notify = types.SendEmail(ctx, w.channels, string(template.Template), translator, notifyUser, colors, request.EventType) case domain.NotificationTypeSms: - notify = types.SendSMS(ctx, w.channels, translator, notifyUser, colors, e, generatorInfo) + notify = types.SendSMS(ctx, w.channels, translator, notifyUser, colors, request.EventType, request.Aggregate.InstanceID, jobID, generatorInfo) } args := request.Args.ToMap() @@ -229,272 +175,12 @@ func (w *NotificationWorker) sendNotification(ctx, txCtx context.Context, tx *sq args[OTP] = code } - if err := notify(request.URLTemplate, args, request.MessageType, request.UnverifiedNotificationChannel); err != nil { + if err = notify(request.URLTemplate, args, request.MessageType, request.UnverifiedNotificationChannel); err != nil { return err } - err = w.commands.NotificationSent(txCtx, tx, e.Aggregate().ID, e.Aggregate().ResourceOwner) - if err != nil { - // In case the notification event cannot be pushed, we most likely cannot create a retry or cancel event. - // Therefore, we'll only log the error and also do not need to try to push to the user / session. - logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "notification", e.Aggregate().ID). - OnError(err).Error("could not set sent notification event") - return nil - } - err = sentHandler(txCtx, w.commands, request.NotificationAggregateID(), request.NotificationAggregateResourceOwner(), generatorInfo, args) - logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "notification", e.Aggregate().ID). + + err = sentHandler(authz.WithInstanceID(ctx, request.Aggregate.InstanceID), w.commands, request.Aggregate.ID, request.Aggregate.ResourceOwner, generatorInfo, args) + logging.WithFields("instanceID", request.Aggregate.InstanceID, "notification", request.Aggregate.ID). OnError(err).Error("could not set notification event on aggregate") return nil } - -func (w *NotificationWorker) exponentialBackOff(current time.Duration) time.Duration { - if current >= w.config.MaxRetryDelay { - return w.config.MaxRetryDelay - } - if current < w.config.MinRetryDelay { - current = w.config.MinRetryDelay - } - t := time.Duration(rand.Int64N(int64(w.config.RetryDelayFactor*float32(current.Nanoseconds()))-current.Nanoseconds()) + current.Nanoseconds()) - if t > w.config.MaxRetryDelay { - return w.config.MaxRetryDelay - } - return t -} - -func notificationEventToRequest(e notification.Request, notifyUser *query.NotifyUser, backoff time.Duration) *command.NotificationRetryRequest { - return &command.NotificationRetryRequest{ - NotificationRequest: command.NotificationRequest{ - UserID: e.UserID, - UserResourceOwner: e.UserResourceOwner, - TriggerOrigin: e.TriggeredAtOrigin, - URLTemplate: e.URLTemplate, - Code: e.Code, - CodeExpiry: e.CodeExpiry, - EventType: e.EventType, - NotificationType: e.NotificationType, - MessageType: e.MessageType, - UnverifiedNotificationChannel: e.UnverifiedNotificationChannel, - Args: e.Args, - AggregateID: e.AggregateID, - AggregateResourceOwner: e.AggregateResourceOwner, - IsOTP: e.IsOTP, - }, - BackOff: backoff, - NotifyUser: notifyUser, - } -} - -func (w *NotificationWorker) schedule(ctx context.Context, workerID int, retry bool) { - t := time.NewTimer(0) - - for { - select { - case <-ctx.Done(): - t.Stop() - w.log(workerID, retry).Info("scheduler stopped") - return - case <-t.C: - instances, err := w.queryInstances(ctx, retry) - w.log(workerID, retry).OnError(err).Error("unable to query instances") - - w.triggerInstances(call.WithTimestamp(ctx), instances, workerID, retry) - if retry { - t.Reset(w.config.RetryRequeueEvery) - continue - } - t.Reset(w.config.RequeueEvery) - } - } -} - -func (w *NotificationWorker) log(workerID int, retry bool) *logging.Entry { - return logging.WithFields("notification worker", workerID, "retries", retry) -} - -func (w *NotificationWorker) queryInstances(ctx context.Context, retry bool) ([]string, error) { - return w.queries.ActiveInstances(), nil -} - -func (w *NotificationWorker) triggerInstances(ctx context.Context, instances []string, workerID int, retry bool) { - for _, instance := range instances { - instanceCtx := authz.WithInstanceID(ctx, instance) - - err := w.trigger(instanceCtx, workerID, retry) - w.log(workerID, retry).WithField("instance", instance).OnError(err).Info("trigger failed") - } -} - -func (w *NotificationWorker) trigger(ctx context.Context, workerID int, retry bool) (err error) { - txCtx := ctx - if w.config.TransactionDuration > 0 { - var cancel, cancelTx func() - txCtx, cancelTx = context.WithCancel(ctx) - defer cancelTx() - ctx, cancel = context.WithTimeout(ctx, w.config.TransactionDuration) - defer cancel() - } - tx, err := w.client.BeginTx(txCtx, nil) - if err != nil { - return err - } - defer func() { - err = database.CloseTransaction(tx, err) - }() - - events, err := w.searchEvents(txCtx, tx, retry) - if err != nil { - return err - } - - // If there aren't any events or no unlocked event terminate early and start a new run. - if len(events) == 0 { - return nil - } - - w.log(workerID, retry). - WithField("instanceID", authz.GetInstance(ctx).InstanceID()). - WithField("events", len(events)). - Info("handling notification events") - - for _, event := range events { - var err error - switch e := event.(type) { - case *notification.RequestedEvent: - w.createSavepoint(txCtx, tx, event, workerID, retry) - err = w.reduceNotificationRequested(ctx, txCtx, tx, e) - case *notification.RetryRequestedEvent: - w.createSavepoint(txCtx, tx, event, workerID, retry) - err = w.reduceNotificationRetry(ctx, txCtx, tx, e) - } - if err != nil { - w.log(workerID, retry).OnError(err). - WithField("instanceID", authz.GetInstance(ctx).InstanceID()). - WithField("notificationID", event.Aggregate().ID). - WithField("sequence", event.Sequence()). - WithField("type", event.Type()). - Error("could not handle notification event") - // if we have an error, we rollback to the savepoint and continue with the next event - // we use the txCtx to make sure we can rollback the transaction in case the ctx is canceled - w.rollbackToSavepoint(txCtx, tx, event, workerID, retry) - } - // if the context is canceled, we stop the processing - if ctx.Err() != nil { - return nil - } - } - return nil -} - -func (w *NotificationWorker) latestRetries(events []eventstore.Event) []eventstore.Event { - for i := len(events) - 1; i > 0; i-- { - // since we delete during the iteration, we need to make sure we don't panic - if len(events) <= i { - continue - } - // delete all the previous retries of the same notification - events = slices.DeleteFunc(events, func(e eventstore.Event) bool { - return e.Aggregate().ID == events[i].Aggregate().ID && - e.Sequence() < events[i].Sequence() - }) - } - return events -} - -func (w *NotificationWorker) createSavepoint(ctx context.Context, tx *sql.Tx, event eventstore.Event, workerID int, retry bool) { - _, err := tx.ExecContext(ctx, "SAVEPOINT notification_send") - w.log(workerID, retry).OnError(err). - WithField("instanceID", authz.GetInstance(ctx).InstanceID()). - WithField("notificationID", event.Aggregate().ID). - WithField("sequence", event.Sequence()). - WithField("type", event.Type()). - Error("could not create savepoint for notification event") -} - -func (w *NotificationWorker) rollbackToSavepoint(ctx context.Context, tx *sql.Tx, event eventstore.Event, workerID int, retry bool) { - _, err := tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT notification_send") - w.log(workerID, retry).OnError(err). - WithField("instanceID", authz.GetInstance(ctx).InstanceID()). - WithField("notificationID", event.Aggregate().ID). - WithField("sequence", event.Sequence()). - WithField("type", event.Type()). - Error("could not rollback to savepoint for notification event") -} - -func (w *NotificationWorker) searchEvents(ctx context.Context, tx *sql.Tx, retry bool) ([]eventstore.Event, error) { - if retry { - return w.searchRetryEvents(ctx, tx) - } - // query events and lock them for update (with skip locked) - searchQuery := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - LockRowsDuringTx(tx, eventstore.LockOptionSkipLocked). - // Messages older than the MaxTTL, we can be ignored. - // The first attempt of a retry might still be older than the TTL and needs to be filtered out later on. - CreationDateAfter(w.now().Add(-1*w.config.MaxTtl)). - Limit(uint64(w.config.BulkLimit)). - AddQuery(). - AggregateTypes(notification.AggregateType). - EventTypes(notification.RequestedType). - Builder(). - ExcludeAggregateIDs(). - EventTypes(notification.RetryRequestedType, notification.CanceledType, notification.SentType). - AggregateTypes(notification.AggregateType). - Builder() - //nolint:staticcheck - return w.es.Filter(ctx, searchQuery) -} - -func (w *NotificationWorker) searchRetryEvents(ctx context.Context, tx *sql.Tx) ([]eventstore.Event, error) { - // query events and lock them for update (with skip locked) - searchQuery := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - LockRowsDuringTx(tx, eventstore.LockOptionSkipLocked). - // Messages older than the MaxTTL, we can be ignored. - // The first attempt of a retry might still be older than the TTL and needs to be filtered out later on. - CreationDateAfter(w.now().Add(-1*w.config.MaxTtl)). - AddQuery(). - AggregateTypes(notification.AggregateType). - EventTypes(notification.RetryRequestedType). - Builder(). - ExcludeAggregateIDs(). - EventTypes(notification.CanceledType, notification.SentType). - AggregateTypes(notification.AggregateType). - Builder() - //nolint:staticcheck - events, err := w.es.Filter(ctx, searchQuery) - if err != nil { - return nil, err - } - return w.latestRetries(events), nil -} - -type existingInstances []string - -// AppendEvents implements eventstore.QueryReducer. -func (ai *existingInstances) AppendEvents(events ...eventstore.Event) { - for _, event := range events { - switch event.Type() { - case instance.InstanceAddedEventType: - *ai = append(*ai, event.Aggregate().InstanceID) - case instance.InstanceRemovedEventType: - *ai = slices.DeleteFunc(*ai, func(s string) bool { - return s == event.Aggregate().InstanceID - }) - } - } -} - -// Query implements eventstore.QueryReducer. -func (*existingInstances) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AddQuery(). - AggregateTypes(instance.AggregateType). - EventTypes( - instance.InstanceAddedEventType, - instance.InstanceRemovedEventType, - ). - Builder() -} - -// Reduce implements eventstore.QueryReducer. -// reduce is not used as events are reduced during AppendEvents -func (*existingInstances) Reduce() error { - return nil -} diff --git a/internal/notification/handlers/notification_worker_test.go b/internal/notification/handlers/notification_worker_test.go index 40b3197d37..90c6de51fe 100644 --- a/internal/notification/handlers/notification_worker_test.go +++ b/internal/notification/handlers/notification_worker_test.go @@ -2,22 +2,21 @@ package handlers import ( "context" - "database/sql" "errors" "fmt" "testing" "time" "github.com/muhlemmer/gu" + "github.com/riverqueue/river" + "github.com/riverqueue/river/rivertype" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/repository" es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock" "github.com/zitadel/zitadel/internal/notification/channels/email" channel_mock "github.com/zitadel/zitadel/internal/notification/channels/mock" @@ -51,7 +50,6 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { name: "too old", test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { codeAlg, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().NotificationCanceled(gomock.Any(), gomock.Any(), notificationID, instanceID, nil).Return(nil) return fieldsWorker{ queries: queries, commands: commands, @@ -62,19 +60,18 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { now: testNow, }, argsWorker{ - event: ¬ification.RequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().Add(-1 * time.Hour), - Typ: notification.RequestedType, - }), - Request: notification.Request{ + job: &river.Job[*notification.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now().Add(-1 * time.Hour), + }, + Args: ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: notificationID, + ResourceOwner: instanceID, + }, UserID: userID, UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", TriggeredAtOrigin: eventOrigin, EventType: user.HumanInviteCodeAddedType, MessageType: domain.InviteUserMessageType, @@ -90,7 +87,12 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { }, }, }, - }, w + }, + wantWorker{ + err: func(tt assert.TestingT, err error, i ...interface{}) bool { + return errors.Is(err, new(river.JobCancelError)) + }, + } }, }, { @@ -99,13 +101,13 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: "Invitation to APP", - Content: expectContent, + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + TriggeringEventType: user.HumanInviteCodeAddedType, } codeAlg, code := cryptoValue(t, ctrl, "testcode") expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().NotificationSent(gomock.Any(), gomock.Any(), notificationID, instanceID).Return(nil) commands.EXPECT().InviteCodeSent(gomock.Any(), orgID, userID).Return(nil) return fieldsWorker{ queries: queries, @@ -117,19 +119,18 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { now: testNow, }, argsWorker{ - event: ¬ification.RequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().UTC(), - Typ: notification.RequestedType, - }), - Request: notification.Request{ + job: &river.Job[*notification.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: userID, + ResourceOwner: orgID, + }, UserID: userID, UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", TriggeredAtOrigin: eventOrigin, EventType: user.HumanInviteCodeAddedType, MessageType: domain.InviteUserMessageType, @@ -145,7 +146,8 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { }, }, }, - }, w + }, + w }, }, { @@ -159,10 +161,13 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { SenderPhoneNumber: "senderNumber", RecipientPhoneNumber: verifiedPhone, Content: expectContent, + TriggeringEventType: session.OTPSMSChallengedType, + InstanceID: instanceID, + JobID: "1", + UserID: userID, } codeAlg, code := cryptoValue(t, ctrl, testCode) expectTemplateWithNotifyUserQueriesSMS(queries) - commands.EXPECT().NotificationSent(gomock.Any(), gomock.Any(), notificationID, instanceID).Return(nil) commands.EXPECT().OTPSMSSent(gomock.Any(), sessionID, instanceID, &senders.CodeGeneratorInfo{ ID: smsProviderID, VerificationID: verificationID, @@ -177,19 +182,19 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { now: testNow, }, argsWorker{ - event: ¬ification.RequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().UTC(), - Typ: notification.RequestedType, - }), - Request: notification.Request{ + job: &river.Job[*notification.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + ID: 1, + }, + Args: ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: sessionID, + ResourceOwner: instanceID, + }, UserID: userID, UserResourceOwner: orgID, - AggregateID: sessionID, - AggregateResourceOwner: instanceID, TriggeredAtOrigin: eventOrigin, EventType: session.OTPSMSChallengedType, MessageType: domain.VerifySMSOTPMessageType, @@ -216,12 +221,12 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) w.message = &messages.Email{ - Recipients: []string{verifiedEmail}, - Subject: "Domain has been claimed", - Content: expectContent, + Recipients: []string{verifiedEmail}, + Subject: "Domain has been claimed", + Content: expectContent, + TriggeringEventType: user.UserDomainClaimedType, } expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().NotificationSent(gomock.Any(), gomock.Any(), notificationID, instanceID).Return(nil) commands.EXPECT().UserDomainClaimedSent(gomock.Any(), orgID, userID).Return(nil) return fieldsWorker{ queries: queries, @@ -233,19 +238,18 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { now: testNow, }, argsWorker{ - event: ¬ification.RequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().UTC(), - Typ: notification.RequestedType, - }), - Request: notification.Request{ + job: &river.Job[*notification.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: userID, + ResourceOwner: orgID, + }, UserID: userID, UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", TriggeredAtOrigin: eventOrigin, EventType: user.UserDomainClaimedType, MessageType: domain.DomainClaimedMessageType, @@ -270,47 +274,17 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: "Invitation to APP", - Content: expectContent, + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + TriggeringEventType: user.HumanInviteCodeAddedType, } w.sendError = sendError + w.err = func(tt assert.TestingT, err error, i ...interface{}) bool { + return errors.Is(err, sendError) + } codeAlg, code := cryptoValue(t, ctrl, "testcode") expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().NotificationRetryRequested(gomock.Any(), gomock.Any(), notificationID, instanceID, - &command.NotificationRetryRequest{ - NotificationRequest: command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", - TriggerOrigin: eventOrigin, - EventType: user.HumanInviteCodeAddedType, - MessageType: domain.InviteUserMessageType, - NotificationType: domain.NotificationTypeEmail, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - CodeExpiry: 1 * time.Hour, - Code: code, - UnverifiedNotificationChannel: true, - IsOTP: false, - RequiresPreviousDomain: false, - Args: &domain.NotificationArguments{ - ApplicationName: "APP", - }, - }, - BackOff: 1 * time.Second, - NotifyUser: &query.NotifyUser{ - ID: userID, - ResourceOwner: orgID, - LastEmail: lastEmail, - VerifiedEmail: verifiedEmail, - PreferredLoginName: preferredLoginName, - LastPhone: lastPhone, - VerifiedPhone: verifiedPhone, - }, - }, - sendError, - ).Return(nil) return fieldsWorker{ queries: queries, commands: commands, @@ -320,22 +294,21 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { userDataCrypto: codeAlg, now: testNow, backOff: testBackOff, - maxAttempts: 2, }, argsWorker{ - event: ¬ification.RequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().UTC(), - Typ: notification.RequestedType, - }), - Request: notification.Request{ + job: &river.Job[*notification.Request]{ + JobRow: &rivertype.JobRow{ + ID: 1, + CreatedAt: time.Now(), + }, + Args: ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: notificationID, + ResourceOwner: instanceID, + }, UserID: userID, UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", TriggeredAtOrigin: eventOrigin, EventType: user.HumanInviteCodeAddedType, MessageType: domain.InviteUserMessageType, @@ -351,7 +324,8 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { }, }, }, - }, w + }, + w }, }, { @@ -360,315 +334,18 @@ func Test_userNotifier_reduceNotificationRequested(t *testing.T) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: "Invitation to APP", - Content: expectContent, + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + TriggeringEventType: user.HumanInviteCodeAddedType, } w.sendError = sendError - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().NotificationCanceled(gomock.Any(), gomock.Any(), notificationID, instanceID, sendError).Return(nil) - return fieldsWorker{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).MockQuerier, - }), - userDataCrypto: codeAlg, - now: testNow, - backOff: testBackOff, - maxAttempts: 1, - }, - argsWorker{ - event: ¬ification.RequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().UTC(), - Seq: 1, - Typ: notification.RequestedType, - }), - Request: notification.Request{ - UserID: userID, - UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", - TriggeredAtOrigin: eventOrigin, - EventType: user.HumanInviteCodeAddedType, - MessageType: domain.InviteUserMessageType, - NotificationType: domain.NotificationTypeEmail, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - CodeExpiry: 1 * time.Hour, - Code: code, - UnverifiedNotificationChannel: true, - IsOTP: false, - RequiresPreviousDomain: false, - Args: &domain.NotificationArguments{ - ApplicationName: "APP", - }, - }, - }, - }, w - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctrl := gomock.NewController(t) - queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - err := newNotificationWorker(t, ctrl, queries, f, a, w).reduceNotificationRequested( - authz.WithInstanceID(context.Background(), instanceID), - authz.WithInstanceID(context.Background(), instanceID), - &sql.Tx{}, - a.event.(*notification.RequestedEvent)) - if w.err != nil { - w.err(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} + w.err = func(tt assert.TestingT, err error, i ...interface{}) bool { + return err != nil + } -func Test_userNotifier_reduceNotificationRetry(t *testing.T) { - testNow := time.Now - testBackOff := func(current time.Duration) time.Duration { - return time.Second - } - sendError := errors.New("send error") - tests := []struct { - name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fieldsWorker, argsWorker, wantWorker) - }{ - { - name: "too old", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { codeAlg, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().NotificationCanceled(gomock.Any(), gomock.Any(), notificationID, instanceID, nil).Return(nil) - return fieldsWorker{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).MockQuerier, - }), - userDataCrypto: codeAlg, - now: testNow, - }, - argsWorker{ - event: ¬ification.RetryRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().Add(-1 * time.Hour), - Typ: notification.RequestedType, - }), - Request: notification.Request{ - UserID: userID, - UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", - TriggeredAtOrigin: eventOrigin, - EventType: user.HumanInviteCodeAddedType, - MessageType: domain.InviteUserMessageType, - NotificationType: domain.NotificationTypeEmail, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - CodeExpiry: 1 * time.Hour, - Code: code, - UnverifiedNotificationChannel: true, - IsOTP: false, - RequiresPreviousDomain: false, - Args: &domain.NotificationArguments{ - ApplicationName: "APP", - }, - }, - BackOff: 1 * time.Second, - NotifyUser: &query.NotifyUser{ - ID: userID, - ResourceOwner: orgID, - LastEmail: lastEmail, - VerifiedEmail: verifiedEmail, - PreferredLoginName: preferredLoginName, - LastPhone: lastPhone, - VerifiedPhone: verifiedPhone, - }, - }, - }, w - }, - }, - { - name: "backoff not done", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { - codeAlg, code := cryptoValue(t, ctrl, "testcode") - return fieldsWorker{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).MockQuerier, - }), - userDataCrypto: codeAlg, - now: testNow, - }, - argsWorker{ - event: ¬ification.RetryRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now(), - Typ: notification.RequestedType, - Seq: 2, - }), - Request: notification.Request{ - UserID: userID, - UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", - TriggeredAtOrigin: eventOrigin, - EventType: user.HumanInviteCodeAddedType, - MessageType: domain.InviteUserMessageType, - NotificationType: domain.NotificationTypeEmail, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - CodeExpiry: 1 * time.Hour, - Code: code, - UnverifiedNotificationChannel: true, - IsOTP: false, - RequiresPreviousDomain: false, - Args: &domain.NotificationArguments{ - ApplicationName: "APP", - }, - }, - BackOff: 10 * time.Second, - NotifyUser: &query.NotifyUser{ - ID: userID, - ResourceOwner: orgID, - LastEmail: lastEmail, - VerifiedEmail: verifiedEmail, - PreferredLoginName: preferredLoginName, - LastPhone: lastPhone, - VerifiedPhone: verifiedPhone, - }, - }, - }, w - }, - }, - { - name: "send ok", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: "Invitation to APP", - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().NotificationSent(gomock.Any(), gomock.Any(), notificationID, instanceID).Return(nil) - commands.EXPECT().InviteCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fieldsWorker{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).MockQuerier, - }), - userDataCrypto: codeAlg, - now: testNow, - maxAttempts: 3, - }, - argsWorker{ - event: ¬ification.RetryRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().Add(-2 * time.Second), - Typ: notification.RequestedType, - Seq: 2, - }), - Request: notification.Request{ - UserID: userID, - UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", - TriggeredAtOrigin: eventOrigin, - EventType: user.HumanInviteCodeAddedType, - MessageType: domain.InviteUserMessageType, - NotificationType: domain.NotificationTypeEmail, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - CodeExpiry: 1 * time.Hour, - Code: code, - UnverifiedNotificationChannel: true, - IsOTP: false, - RequiresPreviousDomain: false, - Args: &domain.NotificationArguments{ - ApplicationName: "APP", - }, - }, - BackOff: 1 * time.Second, - NotifyUser: &query.NotifyUser{ - ID: userID, - ResourceOwner: orgID, - LastEmail: lastEmail, - VerifiedEmail: verifiedEmail, - PreferredLoginName: preferredLoginName, - LastPhone: lastPhone, - VerifiedPhone: verifiedPhone, - }, - }, - }, w - }, - }, - { - name: "send failed, retry", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: "Invitation to APP", - Content: expectContent, - } - w.sendError = sendError - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().NotificationRetryRequested(gomock.Any(), gomock.Any(), notificationID, instanceID, - &command.NotificationRetryRequest{ - NotificationRequest: command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", - TriggerOrigin: eventOrigin, - EventType: user.HumanInviteCodeAddedType, - MessageType: domain.InviteUserMessageType, - NotificationType: domain.NotificationTypeEmail, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - CodeExpiry: 1 * time.Hour, - Code: code, - UnverifiedNotificationChannel: true, - IsOTP: false, - RequiresPreviousDomain: false, - Args: &domain.NotificationArguments{ - ApplicationName: "APP", - }, - }, - BackOff: 1 * time.Second, - NotifyUser: &query.NotifyUser{ - ID: userID, - ResourceOwner: orgID, - LastEmail: lastEmail, - VerifiedEmail: verifiedEmail, - PreferredLoginName: preferredLoginName, - LastPhone: lastPhone, - VerifiedPhone: verifiedPhone, - }, - }, - sendError, - ).Return(nil) + expectTemplateWithNotifyUserQueries(queries, givenTemplate) return fieldsWorker{ queries: queries, commands: commands, @@ -678,23 +355,20 @@ func Test_userNotifier_reduceNotificationRetry(t *testing.T) { userDataCrypto: codeAlg, now: testNow, backOff: testBackOff, - maxAttempts: 3, }, argsWorker{ - event: ¬ification.RetryRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().Add(-2 * time.Second), - Typ: notification.RequestedType, - Seq: 2, - }), - Request: notification.Request{ + job: &river.Job[*notification.Request]{ + JobRow: &rivertype.JobRow{ + CreatedAt: time.Now(), + }, + Args: ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + InstanceID: instanceID, + ID: userID, + ResourceOwner: orgID, + }, UserID: userID, UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", TriggeredAtOrigin: eventOrigin, EventType: user.HumanInviteCodeAddedType, MessageType: domain.InviteUserMessageType, @@ -709,86 +383,9 @@ func Test_userNotifier_reduceNotificationRetry(t *testing.T) { ApplicationName: "APP", }, }, - BackOff: 1 * time.Second, - NotifyUser: &query.NotifyUser{ - ID: userID, - ResourceOwner: orgID, - LastEmail: lastEmail, - VerifiedEmail: verifiedEmail, - PreferredLoginName: preferredLoginName, - LastPhone: lastPhone, - VerifiedPhone: verifiedPhone, - }, }, - }, w - }, - }, - { - name: "send failed (max attempts), cancel", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: "Invitation to APP", - Content: expectContent, - } - w.sendError = sendError - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().NotificationCanceled(gomock.Any(), gomock.Any(), notificationID, instanceID, sendError).Return(nil) - return fieldsWorker{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).MockQuerier, - }), - userDataCrypto: codeAlg, - now: testNow, - backOff: testBackOff, - maxAttempts: 2, }, - argsWorker{ - event: ¬ification.RetryRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - InstanceID: instanceID, - AggregateID: notificationID, - ResourceOwner: sql.NullString{String: instanceID}, - CreationDate: time.Now().Add(-2 * time.Second), - Seq: 2, - Typ: notification.RequestedType, - }), - Request: notification.Request{ - UserID: userID, - UserResourceOwner: orgID, - AggregateID: "", - AggregateResourceOwner: "", - TriggeredAtOrigin: eventOrigin, - EventType: user.HumanInviteCodeAddedType, - MessageType: domain.InviteUserMessageType, - NotificationType: domain.NotificationTypeEmail, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - CodeExpiry: 1 * time.Hour, - Code: code, - UnverifiedNotificationChannel: true, - IsOTP: false, - RequiresPreviousDomain: false, - Args: &domain.NotificationArguments{ - ApplicationName: "APP", - }, - }, - BackOff: 1 * time.Second, - NotifyUser: &query.NotifyUser{ - ID: userID, - ResourceOwner: orgID, - LastEmail: lastEmail, - VerifiedEmail: verifiedEmail, - PreferredLoginName: preferredLoginName, - LastPhone: lastPhone, - VerifiedPhone: verifiedPhone, - }, - }, - }, w + w }, }, } @@ -798,11 +395,9 @@ func Test_userNotifier_reduceNotificationRetry(t *testing.T) { queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - err := newNotificationWorker(t, ctrl, queries, f, a, w).reduceNotificationRetry( + err := newNotificationWorker(t, ctrl, queries, f, w).Work( authz.WithInstanceID(context.Background(), instanceID), - authz.WithInstanceID(context.Background(), instanceID), - &sql.Tx{}, - a.event.(*notification.RetryRequestedEvent), + a.job, ) if w.err != nil { w.err(t, err) @@ -813,22 +408,18 @@ func Test_userNotifier_reduceNotificationRetry(t *testing.T) { } } -func newNotificationWorker(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fieldsWorker, a argsWorker, w wantWorker) *NotificationWorker { +func newNotificationWorker(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fieldsWorker, w wantWorker) *NotificationWorker { queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") channel := channel_mock.NewMockNotificationChannel(ctrl) - if w.err == nil { - if w.message != nil { - w.message.TriggeringEvent = a.event - channel.EXPECT().HandleMessage(w.message).Return(w.sendError) - } - if w.messageSMS != nil { - w.messageSMS.TriggeringEvent = a.event - channel.EXPECT().HandleMessage(w.messageSMS).DoAndReturn(func(message *messages.SMS) error { - message.VerificationID = gu.Ptr(verificationID) - return w.sendError - }) - } + if w.message != nil { + channel.EXPECT().HandleMessage(w.message).Return(w.sendError) + } + if w.messageSMS != nil { + channel.EXPECT().HandleMessage(w.messageSMS).DoAndReturn(func(message *messages.SMS) error { + message.VerificationID = gu.Ptr(verificationID) + return w.sendError + }) } return &NotificationWorker{ commands: f.commands, @@ -878,88 +469,9 @@ func newNotificationWorker(t *testing.T, ctrl *gomock.Controller, queries *mock. }, config: WorkerConfig{ Workers: 1, - BulkLimit: 10, - RequeueEvery: 2 * time.Second, TransactionDuration: 5 * time.Second, - MaxAttempts: f.maxAttempts, MaxTtl: 5 * time.Minute, - MinRetryDelay: 1 * time.Second, - MaxRetryDelay: 10 * time.Second, - RetryDelayFactor: 2, }, - now: f.now, - backOff: f.backOff, - } -} - -func TestNotificationWorker_exponentialBackOff(t *testing.T) { - type fields struct { - config WorkerConfig - } - type args struct { - current time.Duration - } - tests := []struct { - name string - fields fields - args args - wantMin time.Duration - wantMax time.Duration - }{ - { - name: "less than min, min - 1.5*min", - fields: fields{ - config: WorkerConfig{ - MinRetryDelay: 1 * time.Second, - MaxRetryDelay: 5 * time.Second, - RetryDelayFactor: 1.5, - }, - }, - args: args{ - current: 0, - }, - wantMin: 1000 * time.Millisecond, - wantMax: 1500 * time.Millisecond, - }, - { - name: "current, 1.5*current - max", - fields: fields{ - config: WorkerConfig{ - MinRetryDelay: 1 * time.Second, - MaxRetryDelay: 5 * time.Second, - RetryDelayFactor: 1.5, - }, - }, - args: args{ - current: 4 * time.Second, - }, - wantMin: 4000 * time.Millisecond, - wantMax: 5000 * time.Millisecond, - }, - { - name: "max, max", - fields: fields{ - config: WorkerConfig{ - MinRetryDelay: 1 * time.Second, - MaxRetryDelay: 5 * time.Second, - RetryDelayFactor: 1.5, - }, - }, - args: args{ - current: 5 * time.Second, - }, - wantMin: 5000 * time.Millisecond, - wantMax: 5000 * time.Millisecond, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - w := &NotificationWorker{ - config: tt.fields.config, - } - b := w.exponentialBackOff(tt.args.current) - assert.GreaterOrEqual(t, b, tt.wantMin) - assert.LessOrEqual(t, b, tt.wantMax) - }) + now: f.now, } } diff --git a/internal/notification/handlers/queue.go b/internal/notification/handlers/queue.go new file mode 100644 index 0000000000..3d6bc3b463 --- /dev/null +++ b/internal/notification/handlers/queue.go @@ -0,0 +1,13 @@ +package handlers + +import ( + "context" + + "github.com/riverqueue/river" + + "github.com/zitadel/zitadel/internal/queue" +) + +type Queue interface { + Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error +} diff --git a/internal/notification/handlers/quota_notifier.go b/internal/notification/handlers/quota_notifier.go index c455b9955c..f308291243 100644 --- a/internal/notification/handlers/quota_notifier.go +++ b/internal/notification/handlers/quota_notifier.go @@ -36,7 +36,6 @@ func NewQuotaNotifier( queries: queries, channels: channels, }) - } func (*quotaNotifier) Name() string { @@ -72,7 +71,7 @@ func (u *quotaNotifier) reduceNotificationDue(event eventstore.Event) (*handler. if alreadyHandled { return nil } - err = types.SendJSON(ctx, webhook.Config{CallURL: e.CallURL, Method: http.MethodPost}, u.channels, e, e).WithoutTemplate() + err = types.SendJSON(ctx, webhook.Config{CallURL: e.CallURL, Method: http.MethodPost}, u.channels, e, e.Type()).WithoutTemplate() if err != nil { return err } diff --git a/internal/notification/handlers/telemetry_pusher.go b/internal/notification/handlers/telemetry_pusher.go index be41074bc6..7e510a2b4c 100644 --- a/internal/notification/handlers/telemetry_pusher.go +++ b/internal/notification/handlers/telemetry_pusher.go @@ -104,7 +104,7 @@ func (t *telemetryPusher) pushMilestone(ctx context.Context, e *milestone.Reache Type: e.MilestoneType, ReachedDate: e.GetReachedDate(), }, - e, + e.EventType, ).WithoutTemplate(); err != nil { return err } diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index c24b87c2f6..f36f5d828c 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -7,12 +7,13 @@ import ( http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/ui/console" "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/types" + "github.com/zitadel/zitadel/internal/queue" + "github.com/zitadel/zitadel/internal/repository/notification" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -86,9 +87,11 @@ const ( ) type userNotifier struct { - commands Commands queries *NotificationQueries otpEmailTmpl string + + queue Queue + maxAttempts uint8 } func NewUserNotifier( @@ -98,15 +101,17 @@ func NewUserNotifier( queries *NotificationQueries, channels types.ChannelChains, otpEmailTmpl string, - legacyMode bool, + workerConfig WorkerConfig, + queue Queue, ) *handler.Handler { - if legacyMode { + if workerConfig.LegacyEnabled { return NewUserNotifierLegacy(ctx, config, commands, queries, channels, otpEmailTmpl) } return handler.NewHandler(ctx, &config, &userNotifier{ - commands: commands, queries: queries, otpEmailTmpl: otpEmailTmpl, + queue: queue, + maxAttempts: workerConfig.MaxAttempts, }) } @@ -198,7 +203,6 @@ func (u *userNotifier) reduceInitCodeAdded(event eventstore.Event) (*handler.Sta if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-EFe2f", "reduce.wrong.event.type %s", user.HumanInitialCodeAddedType) } - return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error { ctx := HandlerContext(event.Aggregate()) alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, @@ -215,23 +219,26 @@ func (u *userNotifier) reduceInitCodeAdded(event eventstore.Event) (*handler.Sta return err } origin := http_util.DomainContext(ctx).Origin() - return u.commands.RequestNotification( - ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - origin, - e.EventType, - domain.NotificationTypeEmail, - domain.InitCodeMessageType, - ). - WithURLTemplate(login.InitUserLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID)). - WithCode(e.Code, e.Expiry). - WithArgs(&domain.NotificationArguments{ + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InitCodeMessageType, + Code: e.Code, + CodeExpiry: e.Expiry, + IsOTP: false, + UnverifiedNotificationChannel: true, + URLTemplate: login.InitUserLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID), + Args: &domain.NotificationArguments{ AuthRequestID: e.AuthRequestID, - }). - WithUnverifiedChannel(), + }, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -262,23 +269,26 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St return err } origin := http_util.DomainContext(ctx).Origin() - - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - origin, - e.EventType, - domain.NotificationTypeEmail, - domain.VerifyEmailMessageType, - ). - WithURLTemplate(u.emailCodeTemplate(origin, e)). - WithCode(e.Code, e.Expiry). - WithArgs(&domain.NotificationArguments{ + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailMessageType, + Code: e.Code, + CodeExpiry: e.Expiry, + IsOTP: false, + UnverifiedNotificationChannel: true, + URLTemplate: u.emailCodeTemplate(origin, e), + Args: &domain.NotificationArguments{ AuthRequestID: e.AuthRequestID, - }). - WithUnverifiedChannel(), + }, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -315,22 +325,26 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler return err } origin := http_util.DomainContext(ctx).Origin() - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - origin, - e.EventType, - e.NotificationType, - domain.PasswordResetMessageType, - ). - WithURLTemplate(u.passwordCodeTemplate(origin, e)). - WithCode(e.Code, e.Expiry). - WithArgs(&domain.NotificationArguments{ + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: e.NotificationType, + MessageType: domain.PasswordResetMessageType, + Code: e.Code, + CodeExpiry: e.Expiry, + IsOTP: false, + UnverifiedNotificationChannel: true, + URLTemplate: u.passwordCodeTemplate(origin, e), + Args: &domain.NotificationArguments{ AuthRequestID: e.AuthRequestID, - }). - WithUnverifiedChannel(), + }, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -363,19 +377,22 @@ func (u *userNotifier) reduceOTPSMSCodeAdded(event eventstore.Event) (*handler.S if err != nil { return err } - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - http_util.DomainContext(ctx).Origin(), - e.EventType, - domain.NotificationTypeSms, - domain.VerifySMSOTPMessageType, - ). - WithCode(e.Code, e.Expiry). - WithArgs(otpArgs(ctx, e.Expiry)). - WithOTP(), + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(), + EventType: e.EventType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifySMSOTPMessageType, + Code: e.Code, + CodeExpiry: e.Expiry, + IsOTP: true, + Args: otpArgs(ctx, e.Expiry), + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -412,20 +429,22 @@ func (u *userNotifier) reduceSessionOTPSMSChallenged(event eventstore.Event) (*h args := otpArgs(ctx, e.Expiry) args.SessionID = e.Aggregate().ID - return u.commands.RequestNotification(ctx, - s.UserFactor.ResourceOwner, - command.NewNotificationRequest( - s.UserFactor.UserID, - s.UserFactor.ResourceOwner, - http_util.DomainContext(ctx).Origin(), - e.EventType, - domain.NotificationTypeSms, - domain.VerifySMSOTPMessageType, - ). - WithAggregate(e.Aggregate().ID, e.Aggregate().ResourceOwner). - WithCode(e.Code, e.Expiry). - WithOTP(). - WithArgs(args), + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: s.UserFactor.UserID, + UserResourceOwner: s.UserFactor.ResourceOwner, + TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(), + EventType: e.EventType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifySMSOTPMessageType, + Code: e.Code, + CodeExpiry: e.Expiry, + IsOTP: true, + Args: args, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -459,20 +478,23 @@ func (u *userNotifier) reduceOTPEmailCodeAdded(event eventstore.Event) (*handler } args := otpArgs(ctx, e.Expiry) args.AuthRequestID = authRequestID - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - origin, - e.EventType, - domain.NotificationTypeEmail, - domain.VerifyEmailOTPMessageType, - ). - WithURLTemplate(login.OTPLinkTemplate(origin, authRequestID, domain.MFATypeOTPEmail)). - WithCode(e.Code, e.Expiry). - WithOTP(). - WithArgs(args), + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailOTPMessageType, + Code: e.Code, + CodeExpiry: e.Expiry, + IsOTP: true, + URLTemplate: login.OTPLinkTemplate(origin, authRequestID, domain.MFATypeOTPEmail), + Args: args, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -509,21 +531,23 @@ func (u *userNotifier) reduceSessionOTPEmailChallenged(event eventstore.Event) ( args := otpArgs(ctx, e.Expiry) args.SessionID = e.Aggregate().ID - return u.commands.RequestNotification(ctx, - s.UserFactor.ResourceOwner, - command.NewNotificationRequest( - s.UserFactor.UserID, - s.UserFactor.ResourceOwner, - origin, - e.EventType, - domain.NotificationTypeEmail, - domain.VerifyEmailOTPMessageType, - ). - WithAggregate(e.Aggregate().ID, e.Aggregate().ResourceOwner). - WithURLTemplate(u.otpEmailTemplate(origin, e)). - WithCode(e.Code, e.Expiry). - WithOTP(). - WithArgs(args), + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: s.UserFactor.UserID, + UserResourceOwner: s.UserFactor.ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailOTPMessageType, + Code: e.Code, + CodeExpiry: e.Expiry, + IsOTP: true, + URLTemplate: u.otpEmailTemplate(origin, e), + Args: args, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -564,22 +588,24 @@ func (u *userNotifier) reduceDomainClaimed(event eventstore.Event) (*handler.Sta return err } origin := http_util.DomainContext(ctx).Origin() - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - origin, - e.EventType, - domain.NotificationTypeEmail, - domain.DomainClaimedMessageType, - ). - WithURLTemplate(login.LoginLink(origin, e.Aggregate().ResourceOwner)). - WithUnverifiedChannel(). - WithPreviousDomain(). - WithArgs(&domain.NotificationArguments{ + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.DomainClaimedMessageType, + URLTemplate: login.LoginLink(origin, e.Aggregate().ResourceOwner), + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ TempUsername: e.UserName, - }), + }, + RequiresPreviousDomain: true, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -607,21 +633,24 @@ func (u *userNotifier) reducePasswordlessCodeRequested(event eventstore.Event) ( return err } origin := http_util.DomainContext(ctx).Origin() - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - origin, - e.EventType, - domain.NotificationTypeEmail, - domain.PasswordlessRegistrationMessageType, - ). - WithURLTemplate(u.passwordlessCodeTemplate(origin, e)). - WithCode(e.Code, e.Expiry). - WithArgs(&domain.NotificationArguments{ + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordlessRegistrationMessageType, + URLTemplate: u.passwordlessCodeTemplate(origin, e), + Args: &domain.NotificationArguments{ CodeID: e.ID, - }), + }, + CodeExpiry: e.Expiry, + Code: e.Code, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -664,18 +693,20 @@ func (u *userNotifier) reducePasswordChanged(event eventstore.Event) (*handler.S } origin := http_util.DomainContext(ctx).Origin() - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - origin, - e.EventType, - domain.NotificationTypeEmail, - domain.PasswordChangeMessageType, - ). - WithURLTemplate(console.LoginHintLink(origin, "{{.PreferredLoginName}}")). - WithUnverifiedChannel(), + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordChangeMessageType, + URLTemplate: console.LoginHintLink(origin, "{{.PreferredLoginName}}"), + UnverifiedNotificationChannel: true, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -706,21 +737,24 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St return err } - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - http_util.DomainContext(ctx).Origin(), - e.EventType, - domain.NotificationTypeSms, - domain.VerifyPhoneMessageType, - ). - WithCode(e.Code, e.Expiry). - WithUnverifiedChannel(). - WithArgs(&domain.NotificationArguments{ + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(), + EventType: e.EventType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifyPhoneMessageType, + CodeExpiry: e.Expiry, + Code: e.Code, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ Domain: http_util.DomainContext(ctx).RequestedDomain(), - }), + }, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } @@ -755,23 +789,26 @@ func (u *userNotifier) reduceInviteCodeAdded(event eventstore.Event) (*handler.S if applicationName == "" { applicationName = "ZITADEL" } - return u.commands.RequestNotification(ctx, - e.Aggregate().ResourceOwner, - command.NewNotificationRequest( - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - origin, - e.EventType, - domain.NotificationTypeEmail, - domain.InviteUserMessageType, - ). - WithURLTemplate(u.inviteCodeTemplate(origin, e)). - WithCode(e.Code, e.Expiry). - WithUnverifiedChannel(). - WithArgs(&domain.NotificationArguments{ + return u.queue.Insert(ctx, + ¬ification.Request{ + Aggregate: e.Aggregate(), + UserID: e.Aggregate().ID, + UserResourceOwner: e.Aggregate().ResourceOwner, + TriggeredAtOrigin: origin, + EventType: e.EventType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + CodeExpiry: e.Expiry, + Code: e.Code, + UnverifiedNotificationChannel: true, + URLTemplate: u.inviteCodeTemplate(origin, e), + Args: &domain.NotificationArguments{ AuthRequestID: e.AuthRequestID, ApplicationName: applicationName, - }), + }, + }, + queue.WithQueueName(notification.QueueName), + queue.WithMaxAttempts(u.maxAttempts), ) }), nil } diff --git a/internal/notification/handlers/user_notifier_legacy.go b/internal/notification/handlers/user_notifier_legacy.go index 64fc2f014a..1921510bf3 100644 --- a/internal/notification/handlers/user_notifier_legacy.go +++ b/internal/notification/handlers/user_notifier_legacy.go @@ -171,7 +171,7 @@ func (u *userNotifierLegacy) reduceInitCodeAdded(event eventstore.Event) (*handl if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). + err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e.Type()). SendUserInitCode(ctx, notifyUser, code, e.AuthRequestID) if err != nil { if errors.Is(err, &channels.CancelError{}) { @@ -232,7 +232,7 @@ func (u *userNotifierLegacy) reduceEmailCodeAdded(event eventstore.Event) (*hand if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). + err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()). SendEmailVerificationCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID) if err != nil { if errors.Is(err, &channels.CancelError{}) { @@ -296,9 +296,9 @@ func (u *userNotifierLegacy) reducePasswordCodeAdded(event eventstore.Event) (*h return err } generatorInfo := new(senders.CodeGeneratorInfo) - notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e) + notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()) if e.NotificationType == domain.NotificationTypeSms { - notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e, generatorInfo) + notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e.Type(), e.Aggregate().InstanceID, e.ID, generatorInfo) } err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID) if err != nil { @@ -396,7 +396,7 @@ func (u *userNotifierLegacy) reduceOTPSMS( return nil, err } generatorInfo := new(senders.CodeGeneratorInfo) - notify := types.SendSMS(ctx, u.channels, translator, notifyUser, colors, event, generatorInfo) + notify := types.SendSMS(ctx, u.channels, translator, notifyUser, colors, event.Type(), event.Aggregate().InstanceID, event.Aggregate().ID, generatorInfo) err = notify.SendOTPSMSCode(ctx, plainCode, expiry) if err != nil { if errors.Is(err, &channels.CancelError{}) { @@ -522,7 +522,7 @@ func (u *userNotifierLegacy) reduceOTPEmail( if err != nil { return nil, err } - notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event) + notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()) err = notify.SendOTPEmailCode(ctx, url, plainCode, expiry) if err != nil { if errors.Is(err, &channels.CancelError{}) { @@ -576,7 +576,7 @@ func (u *userNotifierLegacy) reduceDomainClaimed(event eventstore.Event) (*handl if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). + err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()). SendDomainClaimed(ctx, notifyUser, e.UserName) if err != nil { if errors.Is(err, &channels.CancelError{}) { @@ -634,7 +634,7 @@ func (u *userNotifierLegacy) reducePasswordlessCodeRequested(event eventstore.Ev if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). + err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()). SendPasswordlessRegistrationLink(ctx, notifyUser, code, e.ID, e.URLTemplate) if err != nil { if errors.Is(err, &channels.CancelError{}) { @@ -697,7 +697,7 @@ func (u *userNotifierLegacy) reducePasswordChanged(event eventstore.Event) (*han if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). + err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()). SendPasswordChange(ctx, notifyUser) if err != nil { if errors.Is(err, &channels.CancelError{}) { @@ -756,7 +756,7 @@ func (u *userNotifierLegacy) reducePhoneCodeAdded(event eventstore.Event) (*hand return err } generatorInfo := new(senders.CodeGeneratorInfo) - if err = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e, generatorInfo). + if err = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e.Type(), e.Aggregate().InstanceID, e.ID, generatorInfo). SendPhoneVerificationCode(ctx, code); err != nil { if errors.Is(err, &channels.CancelError{}) { // if the notification was canceled, we don't want to return the error, so there is no retry @@ -814,7 +814,7 @@ func (u *userNotifierLegacy) reduceInviteCodeAdded(event eventstore.Event) (*han if err != nil { return err } - notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e) + notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()) err = notify.SendInviteCode(ctx, notifyUser, code, e.ApplicationName, e.URLTemplate, e.AuthRequestID) if err != nil { if errors.Is(err, &channels.CancelError{}) { diff --git a/internal/notification/handlers/user_notifier_legacy_test.go b/internal/notification/handlers/user_notifier_legacy_test.go index a0459938d8..a4c24fd196 100644 --- a/internal/notification/handlers/user_notifier_legacy_test.go +++ b/internal/notification/handlers/user_notifier_legacy_test.go @@ -611,328 +611,331 @@ func Test_userNotifierLegacy_reducePasswordCodeAdded(t *testing.T) { tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, wantLegacy) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &wantLegacyEmail{ - email: &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - }, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, + }{ + { + name: "asset url with event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = &wantLegacyEmail{ + email: &messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, }, - }, w - }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = &wantLegacyEmail{ - email: &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - }, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + } + codeAlg, code := cryptoValue(t, ctrl, "testcode") + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, + userDataCrypto: codeAlg, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, { + name: "asset url without event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) + w.message = &wantLegacyEmail{ + email: &messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, }, - }, w - }, - }, { - name: "button url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", eventOrigin, "", testCode, orgID, userID) - w.message = &wantLegacyEmail{ - email: &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - }, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + } + codeAlg, code := cryptoValue(t, ctrl, "testcode") + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, + userDataCrypto: codeAlg, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + }, + }, w + }, + }, { + name: "button url with event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + givenTemplate := "{{.URL}}" + testCode := "testcode" + expectContent := fmt.Sprintf("%s/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", eventOrigin, "", testCode, orgID, userID) + w.message = &wantLegacyEmail{ + email: &messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, }, - }, w - }, - }, { - name: "button url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, orgID, userID) - w.message = &wantLegacyEmail{ - email: &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - }, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + } + codeAlg, code := cryptoValue(t, ctrl, testCode) + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, + userDataCrypto: codeAlg, + SMSTokenCrypto: nil, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, { + name: "button url without event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + givenTemplate := "{{.URL}}" + testCode := "testcode" + expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, orgID, userID) + w.message = &wantLegacyEmail{ + email: &messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, }, - }, w - }, - }, { - name: "button url without event trigger url with authRequestID", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, orgID, userID) - w.message = &wantLegacyEmail{ - email: &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - }, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + } + codeAlg, code := cryptoValue(t, ctrl, testCode) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - AuthRequestID: authRequestID, + userDataCrypto: codeAlg, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + }, + }, w + }, + }, { + name: "button url without event trigger url with authRequestID", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + givenTemplate := "{{.URL}}" + testCode := "testcode" + expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, orgID, userID) + w.message = &wantLegacyEmail{ + email: &messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, }, - }, w - }, - }, { - name: "button url with url template and event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - givenTemplate := "{{.URL}}" - urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" - testCode := "testcode" - expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) - w.message = &wantLegacyEmail{ - email: &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - }, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + } + codeAlg, code := cryptoValue(t, ctrl, testCode) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: urlTemplate, - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, + userDataCrypto: codeAlg, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + AuthRequestID: authRequestID, + }, + }, w + }, + }, { + name: "button url with url template and event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + givenTemplate := "{{.URL}}" + urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" + testCode := "testcode" + expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) + w.message = &wantLegacyEmail{ + email: &messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, }, - }, w - }, - }, { - name: "external code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - givenTemplate := "{{.URL}}" - expectContent := "We received a password reset request. Please use the button below to reset your password. (Code ) If you didn't ask for this mail, please ignore it." - w.messageSMS = &wantLegacySMS{ - sms: &messages.SMS{ - SenderPhoneNumber: "senderNumber", - RecipientPhoneNumber: lastPhone, - Content: expectContent, - }, - } - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + } + codeAlg, code := cryptoValue(t, ctrl, testCode) + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: nil, - Expiry: 0, - URLTemplate: "", - CodeReturned: false, - NotificationType: domain.NotificationTypeSms, - GeneratorID: smsProviderID, - TriggeredAtOrigin: eventOrigin, + userDataCrypto: codeAlg, + SMSTokenCrypto: nil, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + URLTemplate: urlTemplate, + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, { + name: "external code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + givenTemplate := "{{.URL}}" + expectContent := "We received a password reset request. Please use the button below to reset your password. (Code ) If you didn't ask for this mail, please ignore it." + w.messageSMS = &wantLegacySMS{ + sms: &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: lastPhone, + Content: expectContent, + UserID: userID, }, - }, w - }, - }, { - name: "cancel error, no reduce error expected", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - givenTemplate := "{{.URL}}" - expectContent := "We received a password reset request. Please use the button below to reset your password. (Code ) If you didn't ask for this mail, please ignore it." - w.messageSMS = &wantLegacySMS{ - sms: &messages.SMS{ - SenderPhoneNumber: "senderNumber", - RecipientPhoneNumber: lastPhone, - Content: expectContent, - }, - err: channels.NewCancelError(nil), - } - expectTemplateWithNotifyUserQueries(queries, givenTemplate) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + } + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: nil, - Expiry: 0, - URLTemplate: "", - CodeReturned: false, - NotificationType: domain.NotificationTypeSms, - GeneratorID: smsProviderID, - TriggeredAtOrigin: eventOrigin, + SMSTokenCrypto: nil, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: nil, + Expiry: 0, + URLTemplate: "", + CodeReturned: false, + NotificationType: domain.NotificationTypeSms, + GeneratorID: smsProviderID, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, { + name: "cancel error, no reduce error expected", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + givenTemplate := "{{.URL}}" + expectContent := "We received a password reset request. Please use the button below to reset your password. (Code ) If you didn't ask for this mail, please ignore it." + w.messageSMS = &wantLegacySMS{ + sms: &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: lastPhone, + Content: expectContent, + UserID: userID, }, - }, w + err: channels.NewCancelError(nil), + } + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + SMSTokenCrypto: nil, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: nil, + Expiry: 0, + URLTemplate: "", + CodeReturned: false, + NotificationType: domain.NotificationTypeSms, + GeneratorID: smsProviderID, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1774,131 +1777,138 @@ func Test_userNotifierLegacy_reduceOTPSMSChallenged(t *testing.T) { tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, wantLegacy) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - testCode := "" - expiry := 0 * time.Hour - expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. + }{ + { + name: "asset url with event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + testCode := "" + expiry := 0 * time.Hour + expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. @%[2]s #%[1]s`, testCode, eventOriginDomain, expiry) - w.messageSMS = &wantLegacySMS{ - sms: &messages.SMS{ - SenderPhoneNumber: "senderNumber", - RecipientPhoneNumber: verifiedPhone, - Content: expectContent, - }, - } - expectTemplateWithNotifyUserQueriesSMS(queries) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any(), nil).Return(&query.Session{}, nil) - commands.EXPECT().OTPSMSSent(gomock.Any(), userID, orgID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - }, args{ - event: &session.OTPSMSChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: nil, - Expiry: expiry, - CodeReturned: false, - GeneratorID: smsProviderID, - TriggeredAtOrigin: eventOrigin, + w.messageSMS = &wantLegacySMS{ + sms: &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: verifiedPhone, + Content: expectContent, + JobID: userID, + UserID: userID, }, - }, w - }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - testCode := "" - expiry := 0 * time.Hour - expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. + } + expectTemplateWithNotifyUserQueriesSMS(queries) + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any(), nil).Return(&query.Session{}, nil) + commands.EXPECT().OTPSMSSent(gomock.Any(), userID, orgID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: nil, + Expiry: expiry, + CodeReturned: false, + GeneratorID: smsProviderID, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, { + name: "asset url without event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + testCode := "" + expiry := 0 * time.Hour + expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. @%[2]s #%[1]s`, testCode, instancePrimaryDomain, expiry) - w.messageSMS = &wantLegacySMS{ - sms: &messages.SMS{ - SenderPhoneNumber: "senderNumber", - RecipientPhoneNumber: verifiedPhone, - Content: expectContent, - }, - } - expectTemplateWithNotifyUserQueriesSMS(queries) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any(), nil).Return(&query.Session{}, nil) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - commands.EXPECT().OTPSMSSent(gomock.Any(), userID, orgID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - }, args{ - event: &session.OTPSMSChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: nil, - Expiry: expiry, - CodeReturned: false, - GeneratorID: smsProviderID, + w.messageSMS = &wantLegacySMS{ + sms: &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: verifiedPhone, + Content: expectContent, + JobID: userID, + UserID: userID, }, - }, w - }, - }, { - name: "cancel error, no reduce error expected", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { - testCode := "" - expiry := 0 * time.Hour - expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. + } + expectTemplateWithNotifyUserQueriesSMS(queries) + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any(), nil).Return(&query.Session{}, nil) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + commands.EXPECT().OTPSMSSent(gomock.Any(), userID, orgID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: nil, + Expiry: expiry, + CodeReturned: false, + GeneratorID: smsProviderID, + }, + }, w + }, + }, { + name: "cancel error, no reduce error expected", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w wantLegacy) { + testCode := "" + expiry := 0 * time.Hour + expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. @%[2]s #%[1]s`, testCode, instancePrimaryDomain, expiry) - w.messageSMS = &wantLegacySMS{ - sms: &messages.SMS{ - SenderPhoneNumber: "senderNumber", - RecipientPhoneNumber: verifiedPhone, - Content: expectContent, - }, - err: channels.NewCancelError(nil), - } - expectTemplateWithNotifyUserQueriesSMS(queries) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any(), nil).Return(&query.Session{}, nil) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - }, args{ - event: &session.OTPSMSChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: nil, - Expiry: expiry, - CodeReturned: false, - GeneratorID: smsProviderID, + w.messageSMS = &wantLegacySMS{ + sms: &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: verifiedPhone, + Content: expectContent, + JobID: userID, + UserID: userID, }, - }, w + err: channels.NewCancelError(nil), + } + expectTemplateWithNotifyUserQueriesSMS(queries) + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any(), nil).Return(&query.Session{}, nil) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: nil, + Expiry: expiry, + CodeReturned: false, + GeneratorID: smsProviderID, + }, + }, w + }, }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1938,11 +1948,11 @@ func newUserNotifierLegacy(t *testing.T, ctrl *gomock.Controller, queries *mock. channel := channel_mock.NewMockNotificationChannel(ctrl) if w.err == nil { if w.message != nil { - w.message.email.TriggeringEvent = a.event + w.message.email.TriggeringEventType = a.event.Type() channel.EXPECT().HandleMessage(w.message.email).Return(w.message.err) } if w.messageSMS != nil { - w.messageSMS.sms.TriggeringEvent = a.event + w.messageSMS.sms.TriggeringEventType = a.event.Type() channel.EXPECT().HandleMessage(w.messageSMS.sms).DoAndReturn(func(message *messages.SMS) error { message.VerificationID = gu.Ptr(verificationID) return w.messageSMS.err diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index b7b7ceb446..f7090f0146 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -7,11 +7,11 @@ import ( "testing" "time" + "github.com/riverqueue/river" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -26,6 +26,7 @@ import ( "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/types" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/notification" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" ) @@ -61,33 +62,41 @@ const ( func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{ { name: "with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, q *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/login/user/init?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&passwordset={{.PasswordSet}}&authRequestID=%s", - eventOrigin, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanInitialCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.InitCodeMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + q.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/user/init?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&passwordset={{.PasswordSet}}&authRequestID=%s", + eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInitialCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InitCodeMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: q, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -110,7 +119,7 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { }, { name: "without event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ @@ -118,27 +127,35 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { IsPrimary: true, }}, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/user/init?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&passwordset={{.PasswordSet}}&authRequestID=%s", - externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanInitialCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.InitCodeMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/user/init?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&passwordset={{.PasswordSet}}&authRequestID=%s", + externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInitialCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InitCodeMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -163,9 +180,9 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceInitCodeAdded(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reduceInitCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -184,33 +201,41 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{ { name: "with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/login/mail/verification?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", - eventOrigin, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanEmailCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.VerifyEmailMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/mail/verification?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanEmailCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -235,7 +260,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, { name: "without event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ @@ -244,27 +269,35 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }}, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", - externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanEmailCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.VerifyEmailMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanEmailCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -288,12 +321,12 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, { name: "return code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { w.noOperation = true _, code := cryptoValue(t, ctrl, "testcode") return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).MockQuerier, }), @@ -321,9 +354,9 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceEmailCodeAdded(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reduceEmailCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -346,33 +379,41 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{ { name: "with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", - eventOrigin, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanPasswordCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.PasswordResetMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanPasswordCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordResetMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -397,7 +438,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }, { name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ @@ -405,27 +446,35 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { IsPrimary: true, }}, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", - externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanPasswordCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.PasswordResetMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanPasswordCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordResetMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -449,28 +498,36 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }, { name: "external code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", - eventOrigin, userID, orgID, authRequestID), - Code: nil, - CodeExpiry: 0, - EventType: user.HumanPasswordCodeAddedType, - NotificationType: domain.NotificationTypeSms, - MessageType: domain.PasswordResetMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + eventOrigin, userID, orgID, authRequestID), + Code: nil, + CodeExpiry: 0, + EventType: user.HumanPasswordCodeAddedType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.PasswordResetMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -497,12 +554,12 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }, { name: "return code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { w.noOperation = true _, code := cryptoValue(t, ctrl, "testcode") return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).MockQuerier, }), @@ -532,9 +589,9 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordCodeAdded(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reducePasswordCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -557,31 +614,39 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { func Test_userNotifier_reduceDomainClaimed(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{{ name: "with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/login/login?orgID=%s", - eventOrigin, orgID), - Code: nil, - CodeExpiry: 0, - EventType: user.UserDomainClaimedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.DomainClaimedMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{TempUsername: "newUsername"}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: true, - }).Return(nil) + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/login?orgID=%s", + eventOrigin, orgID), + Code: nil, + CodeExpiry: 0, + EventType: user.UserDomainClaimedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.DomainClaimedMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{TempUsername: "newUsername"}, + IsOTP: false, + RequiresPreviousDomain: true, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -601,34 +666,42 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { }, }, { name: "without event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ Domain: instancePrimaryDomain, IsPrimary: true, }}, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/login?orgID=%s", - externalProtocol, instancePrimaryDomain, externalPort, orgID), - Code: nil, - CodeExpiry: 0, - EventType: user.UserDomainClaimedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.DomainClaimedMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{TempUsername: "newUsername"}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: true, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/login?orgID=%s", + externalProtocol, instancePrimaryDomain, externalPort, orgID), + Code: nil, + CodeExpiry: 0, + EventType: user.UserDomainClaimedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.DomainClaimedMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{TempUsername: "newUsername"}, + IsOTP: false, + RequiresPreviousDomain: true, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -650,9 +723,9 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceDomainClaimed(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reduceDomainClaimed(a.event) if w.err != nil { w.err(t, err) } else { @@ -671,32 +744,40 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{ { name: "with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code={{.Code}}", eventOrigin, userID, orgID, codeID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanPasswordlessInitCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.PasswordlessRegistrationMessageType, - UnverifiedNotificationChannel: false, - Args: &domain.NotificationArguments{CodeID: codeID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code={{.Code}}", eventOrigin, userID, orgID, codeID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanPasswordlessInitCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordlessRegistrationMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{CodeID: codeID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -721,7 +802,7 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { }, { name: "without event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testCode") queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ @@ -729,26 +810,34 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { IsPrimary: true, }}, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code={{.Code}}", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, codeID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanPasswordlessInitCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.PasswordlessRegistrationMessageType, - UnverifiedNotificationChannel: false, - Args: &domain.NotificationArguments{CodeID: codeID}, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code={{.Code}}", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, codeID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanPasswordlessInitCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordlessRegistrationMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{CodeID: codeID}, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -772,12 +861,12 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { }, { name: "return code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { w.noOperation = true _, code := cryptoValue(t, ctrl, "testcode") return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).MockQuerier, }), @@ -805,9 +894,9 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordlessCodeRequested(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reducePasswordlessCodeRequested(a.event) if w.err != nil { w.err(t, err) } else { @@ -830,34 +919,42 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { func Test_userNotifier_reducePasswordChanged(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{ { name: "with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{ PasswordChange: true, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/console?login_hint={{.PreferredLoginName}}", eventOrigin), - Code: nil, - CodeExpiry: 0, - EventType: user.HumanPasswordChangedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.PasswordChangeMessageType, - UnverifiedNotificationChannel: true, - Args: nil, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/console?login_hint={{.PreferredLoginName}}", eventOrigin), + Code: nil, + CodeExpiry: 0, + EventType: user.HumanPasswordChangedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordChangeMessageType, + UnverifiedNotificationChannel: true, + Args: nil, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -877,7 +974,7 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { }, { name: "without event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{ PasswordChange: true, }, nil) @@ -887,27 +984,35 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { IsPrimary: true, }}, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: fmt.Sprintf("%s://%s:%d/ui/console?login_hint={{.PreferredLoginName}}", - externalProtocol, instancePrimaryDomain, externalPort), - Code: nil, - CodeExpiry: 0, - EventType: user.HumanPasswordChangedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.PasswordChangeMessageType, - UnverifiedNotificationChannel: true, - Args: nil, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/console?login_hint={{.PreferredLoginName}}", + externalProtocol, instancePrimaryDomain, externalPort), + Code: nil, + CodeExpiry: 0, + EventType: user.HumanPasswordChangedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordChangeMessageType, + UnverifiedNotificationChannel: true, + Args: nil, + IsOTP: false, + RequiresPreviousDomain: false, + }, + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -925,13 +1030,13 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { }, }, { name: "no notification", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{ PasswordChange: false, }, nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -953,9 +1058,9 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordChanged(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reducePasswordChanged(a.event) if w.err != nil { w.err(t, err) } else { @@ -974,11 +1079,11 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{ { name: "url with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testCode") queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any(), nil).Return(&query.Session{ ID: sessionID, @@ -988,31 +1093,39 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { ResourceOwner: orgID, }, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/otp/verify?loginName={{.LoginName}}&code={{.Code}}", eventOrigin), - Code: code, - CodeExpiry: time.Hour, - EventType: session.OTPEmailChallengedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.VerifyEmailOTPMessageType, - UnverifiedNotificationChannel: false, - Args: &domain.NotificationArguments{ - Domain: eventOriginDomain, - Expiry: 1 * time.Hour, - Origin: eventOrigin, - SessionID: sessionID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + Aggregate: &eventstore.Aggregate{ + ID: sessionID, + InstanceID: instanceID, + ResourceOwner: instanceID, + }, + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/otp/verify?loginName={{.LoginName}}&code={{.Code}}", eventOrigin), + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPEmailChallengedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: eventOriginDomain, + Expiry: 1 * time.Hour, + Origin: eventOrigin, + SessionID: sessionID, + }, + IsOTP: true, + RequiresPreviousDomain: false, }, - AggregateID: sessionID, - AggregateResourceOwner: instanceID, - IsOTP: true, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1036,7 +1149,7 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { }, { name: "without event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testCode") queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ @@ -1052,31 +1165,39 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { ResourceOwner: orgID, }, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: fmt.Sprintf("%s://%s:%d/otp/verify?loginName={{.LoginName}}&code={{.Code}}", externalProtocol, instancePrimaryDomain, externalPort), - Code: code, - CodeExpiry: time.Hour, - EventType: session.OTPEmailChallengedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.VerifyEmailOTPMessageType, - UnverifiedNotificationChannel: false, - Args: &domain.NotificationArguments{ - Domain: instancePrimaryDomain, - Expiry: 1 * time.Hour, - Origin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - SessionID: sessionID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/otp/verify?loginName={{.LoginName}}&code={{.Code}}", externalProtocol, instancePrimaryDomain, externalPort), + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPEmailChallengedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: instancePrimaryDomain, + Expiry: 1 * time.Hour, + Origin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + SessionID: sessionID, + }, + Aggregate: &eventstore.Aggregate{ + ID: sessionID, + InstanceID: instanceID, + ResourceOwner: instanceID, + }, + IsOTP: true, + RequiresPreviousDomain: false, }, - AggregateID: sessionID, - AggregateResourceOwner: instanceID, - IsOTP: true, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1098,12 +1219,12 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { }, { name: "return code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { w.noOperation = true _, code := cryptoValue(t, ctrl, "testCode") return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).MockQuerier, }), @@ -1127,7 +1248,7 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { }, { name: "url template", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testCode") queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any(), nil).Return(&query.Session{ ID: sessionID, @@ -1137,31 +1258,39 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { ResourceOwner: orgID, }, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: "/verify-otp?sessionID={{.SessionID}}", - Code: code, - CodeExpiry: time.Hour, - EventType: session.OTPEmailChallengedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.VerifyEmailOTPMessageType, - UnverifiedNotificationChannel: false, - Args: &domain.NotificationArguments{ - Domain: eventOriginDomain, - Expiry: 1 * time.Hour, - Origin: eventOrigin, - SessionID: sessionID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: "/verify-otp?sessionID={{.SessionID}}", + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPEmailChallengedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: eventOriginDomain, + Expiry: 1 * time.Hour, + Origin: eventOrigin, + SessionID: sessionID, + }, + Aggregate: &eventstore.Aggregate{ + ID: sessionID, + InstanceID: instanceID, + ResourceOwner: instanceID, + }, + IsOTP: true, + RequiresPreviousDomain: false, }, - AggregateID: sessionID, - AggregateResourceOwner: instanceID, - IsOTP: true, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1188,9 +1317,9 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPEmailChallenged(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reduceSessionOTPEmailChallenged(a.event) if w.err != nil { w.err(t, err) } else { @@ -1213,11 +1342,11 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{ { name: "with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { testCode := "testcode" _, code := cryptoValue(t, ctrl, testCode) queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any(), nil).Return(&query.Session{ @@ -1228,31 +1357,39 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { ResourceOwner: orgID, }, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: "", - Code: code, - CodeExpiry: time.Hour, - EventType: session.OTPSMSChallengedType, - NotificationType: domain.NotificationTypeSms, - MessageType: domain.VerifySMSOTPMessageType, - UnverifiedNotificationChannel: false, - Args: &domain.NotificationArguments{ - Domain: eventOriginDomain, - Expiry: 1 * time.Hour, - Origin: eventOrigin, - SessionID: sessionID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: "", + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPSMSChallengedType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifySMSOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: eventOriginDomain, + Expiry: 1 * time.Hour, + Origin: eventOrigin, + SessionID: sessionID, + }, + Aggregate: &eventstore.Aggregate{ + ID: sessionID, + InstanceID: instanceID, + ResourceOwner: instanceID, + }, + IsOTP: true, + RequiresPreviousDomain: false, }, - AggregateID: sessionID, - AggregateResourceOwner: instanceID, - IsOTP: true, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1275,7 +1412,7 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { }, { name: "without event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { testCode := "testcode" _, code := cryptoValue(t, ctrl, testCode) queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ @@ -1292,31 +1429,39 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { ResourceOwner: orgID, }, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: "", - Code: code, - CodeExpiry: time.Hour, - EventType: session.OTPSMSChallengedType, - NotificationType: domain.NotificationTypeSms, - MessageType: domain.VerifySMSOTPMessageType, - UnverifiedNotificationChannel: false, - Args: &domain.NotificationArguments{ - Domain: instancePrimaryDomain, - Expiry: 1 * time.Hour, - Origin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - SessionID: sessionID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: "", + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPSMSChallengedType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifySMSOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: instancePrimaryDomain, + Expiry: 1 * time.Hour, + Origin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + SessionID: sessionID, + }, + Aggregate: &eventstore.Aggregate{ + ID: sessionID, + InstanceID: instanceID, + ResourceOwner: instanceID, + }, + IsOTP: true, + RequiresPreviousDomain: false, }, - AggregateID: sessionID, - AggregateResourceOwner: instanceID, - IsOTP: true, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1338,7 +1483,7 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { }, { name: "external code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any(), nil).Return(&query.Session{ ID: sessionID, ResourceOwner: instanceID, @@ -1347,31 +1492,39 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { ResourceOwner: orgID, }, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: "", - Code: nil, - CodeExpiry: 0, - EventType: session.OTPSMSChallengedType, - NotificationType: domain.NotificationTypeSms, - MessageType: domain.VerifySMSOTPMessageType, - UnverifiedNotificationChannel: false, - Args: &domain.NotificationArguments{ - Domain: eventOriginDomain, - Expiry: 0 * time.Hour, - Origin: eventOrigin, - SessionID: sessionID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: "", + Code: nil, + CodeExpiry: 0, + EventType: session.OTPSMSChallengedType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifySMSOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: eventOriginDomain, + Expiry: 0 * time.Hour, + Origin: eventOrigin, + SessionID: sessionID, + }, + Aggregate: &eventstore.Aggregate{ + ID: sessionID, + InstanceID: instanceID, + ResourceOwner: instanceID, + }, + IsOTP: true, + RequiresPreviousDomain: false, }, - AggregateID: sessionID, - AggregateResourceOwner: instanceID, - IsOTP: true, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1394,12 +1547,12 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { }, { name: "return code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { w.noOperation = true _, code := cryptoValue(t, ctrl, "testCode") return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).MockQuerier, }), @@ -1425,9 +1578,9 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPSMSChallenged(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reduceSessionOTPSMSChallenged(a.event) if w.err != nil { w.err(t, err) } else { @@ -1450,35 +1603,43 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { tests := []struct { name string - test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockQueue) (fields, args, want) }{ { name: "with event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanInviteCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.InviteUserMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{ - ApplicationName: "ZITADEL", - AuthRequestID: authRequestID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInviteCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ + ApplicationName: "ZITADEL", + AuthRequestID: authRequestID, + }, + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + IsOTP: false, + RequiresPreviousDomain: false, }, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1503,7 +1664,7 @@ func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { }, { name: "without event trigger", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testCode") queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ @@ -1511,29 +1672,37 @@ func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { IsPrimary: true, }}, }, nil) - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), - URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanInviteCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.InviteUserMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{ - ApplicationName: "ZITADEL", - AuthRequestID: authRequestID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInviteCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ + ApplicationName: "ZITADEL", + AuthRequestID: authRequestID, + }, + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + IsOTP: false, + RequiresPreviousDomain: false, }, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1557,12 +1726,12 @@ func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { }, { name: "return code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { w.noOperation = true _, code := cryptoValue(t, ctrl, "testcode") return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).MockQuerier, }), @@ -1587,31 +1756,39 @@ func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { }, { name: "url template", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: "/passwordless-init?userID={{.UserID}}", - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanInviteCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.InviteUserMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{ - ApplicationName: "ZITADEL", - AuthRequestID: authRequestID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: "/passwordless-init?userID={{.UserID}}", + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInviteCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ + ApplicationName: "ZITADEL", + AuthRequestID: authRequestID, + }, + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + IsOTP: false, + RequiresPreviousDomain: false, }, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1636,31 +1813,39 @@ func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { }, { name: "application name", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { _, code := cryptoValue(t, ctrl, "testcode") - commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ - UserID: userID, - UserResourceOwner: orgID, - TriggerOrigin: eventOrigin, - URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), - Code: code, - CodeExpiry: time.Hour, - EventType: user.HumanInviteCodeAddedType, - NotificationType: domain.NotificationTypeEmail, - MessageType: domain.InviteUserMessageType, - UnverifiedNotificationChannel: true, - Args: &domain.NotificationArguments{ - ApplicationName: "APP", - AuthRequestID: authRequestID, + queue.EXPECT().Insert( + gomock.Any(), + ¬ification.Request{ + UserID: userID, + UserResourceOwner: orgID, + TriggeredAtOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInviteCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + AuthRequestID: authRequestID, + }, + Aggregate: &eventstore.Aggregate{ + ID: userID, + InstanceID: instanceID, + ResourceOwner: orgID, + }, + IsOTP: false, + RequiresPreviousDomain: false, }, - AggregateID: "", - AggregateResourceOwner: "", - IsOTP: false, - RequiresPreviousDomain: false, - }).Return(nil) + gomock.Any(), + gomock.Any(), + ).Return(nil) return fields{ - queries: queries, - commands: commands, + queries: queries, + queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), @@ -1689,9 +1874,9 @@ func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) - commands := mock.NewMockCommands(ctrl) - f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceInviteCodeAdded(a.event) + queue := mock.NewMockQueue(ctrl) + f, a, w := tt.test(ctrl, queries, queue) + stmt, err := newUserNotifier(t, ctrl, queries, f).reduceInviteCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -1713,6 +1898,7 @@ func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { type fields struct { queries *mock.MockQueries + queue *mock.MockQueue commands *mock.MockCommands es *eventstore.Eventstore userDataCrypto crypto.EncryptionAlgorithm @@ -1726,13 +1912,12 @@ type fieldsWorker struct { SMSTokenCrypto crypto.EncryptionAlgorithm now nowFunc backOff func(current time.Duration) time.Duration - maxAttempts uint8 } type args struct { event eventstore.Event } type argsWorker struct { - event eventstore.Event + job *river.Job[*notification.Request] } type want struct { noOperation bool @@ -1745,11 +1930,11 @@ type wantWorker struct { err assert.ErrorAssertionFunc } -func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fields, a args, w want) *userNotifier { +func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fields) *userNotifier { queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") return &userNotifier{ - commands: f.commands, + queue: f.queue, queries: NewNotificationQueries( f.queries, f.es, diff --git a/internal/notification/messages/email.go b/internal/notification/messages/email.go index ff5931898f..019d1d09d8 100644 --- a/internal/notification/messages/email.go +++ b/internal/notification/messages/email.go @@ -19,15 +19,15 @@ var ( var _ channels.Message = (*Email)(nil) type Email struct { - Recipients []string - BCC []string - CC []string - SenderEmail string - SenderName string - ReplyToAddress string - Subject string - Content string - TriggeringEvent eventstore.Event + Recipients []string + BCC []string + CC []string + SenderEmail string + SenderName string + ReplyToAddress string + Subject string + Content string + TriggeringEventType eventstore.EventType } func (msg *Email) GetContent() (string, error) { @@ -61,8 +61,8 @@ func (msg *Email) GetContent() (string, error) { return message, nil } -func (msg *Email) GetTriggeringEvent() eventstore.Event { - return msg.TriggeringEvent +func (msg *Email) GetTriggeringEventType() eventstore.EventType { + return msg.TriggeringEventType } func isHTML(input string) bool { diff --git a/internal/notification/messages/form.go b/internal/notification/messages/form.go index 5e9a97ca68..1f3b22e39c 100644 --- a/internal/notification/messages/form.go +++ b/internal/notification/messages/form.go @@ -12,8 +12,8 @@ import ( var _ channels.Message = (*Form)(nil) type Form struct { - Serializable any - TriggeringEvent eventstore.Event + Serializable any + TriggeringEventType eventstore.EventType } func (msg *Form) GetContent() (string, error) { @@ -22,6 +22,6 @@ func (msg *Form) GetContent() (string, error) { return values.Encode(), err } -func (msg *Form) GetTriggeringEvent() eventstore.Event { - return msg.TriggeringEvent +func (msg *Form) GetTriggeringEventType() eventstore.EventType { + return msg.TriggeringEventType } diff --git a/internal/notification/messages/json.go b/internal/notification/messages/json.go index be092f430b..5abc21873f 100644 --- a/internal/notification/messages/json.go +++ b/internal/notification/messages/json.go @@ -10,8 +10,8 @@ import ( var _ channels.Message = (*JSON)(nil) type JSON struct { - Serializable interface{} - TriggeringEvent eventstore.Event + Serializable interface{} + TriggeringEventType eventstore.EventType } func (msg *JSON) GetContent() (string, error) { @@ -19,6 +19,6 @@ func (msg *JSON) GetContent() (string, error) { return string(bytes), err } -func (msg *JSON) GetTriggeringEvent() eventstore.Event { - return msg.TriggeringEvent +func (msg *JSON) GetTriggeringEventType() eventstore.EventType { + return msg.TriggeringEventType } diff --git a/internal/notification/messages/sms.go b/internal/notification/messages/sms.go index 0dfaea8772..8c53531242 100644 --- a/internal/notification/messages/sms.go +++ b/internal/notification/messages/sms.go @@ -11,16 +11,19 @@ type SMS struct { SenderPhoneNumber string RecipientPhoneNumber string Content string - TriggeringEvent eventstore.Event + TriggeringEventType eventstore.EventType // VerificationID is set by the sender VerificationID *string + InstanceID string + JobID string + UserID string } func (msg *SMS) GetContent() (string, error) { return msg.Content, nil } -func (msg *SMS) GetTriggeringEvent() eventstore.Event { - return msg.TriggeringEvent +func (msg *SMS) GetTriggeringEventType() eventstore.EventType { + return msg.TriggeringEventType } diff --git a/internal/notification/projections.go b/internal/notification/projections.go index 38e1f1c347..a2d4d4140e 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -6,18 +6,17 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/notification/handlers" _ "github.com/zitadel/zitadel/internal/notification/statik" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/queue" ) var ( projections []*handler.Handler - worker *handlers.NotificationWorker ) func Register( @@ -34,11 +33,15 @@ func Register( otpEmailTmpl, fileSystemPath string, userEncryption, smtpEncryption, smsEncryption, keysEncryptionAlg crypto.EncryptionAlgorithm, tokenLifetime time.Duration, - client *database.DB, + queue *queue.Queue, ) { + if !notificationWorkerConfig.LegacyEnabled { + queue.ShouldStart() + } + q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption) c := newChannels(q) - projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl, notificationWorkerConfig.LegacyEnabled)) + projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl, notificationWorkerConfig, queue)) projections = append(projections, handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c)) projections = append(projections, handlers.NewBackChannelLogoutNotifier( ctx, @@ -53,14 +56,15 @@ func Register( if telemetryCfg.Enabled { projections = append(projections, handlers.NewTelemetryPusher(ctx, telemetryCfg, projection.ApplyCustomConfig(telemetryHandlerCustomConfig), commands, q, c)) } - worker = handlers.NewNotificationWorker(notificationWorkerConfig, commands, q, es, client, c) + if !notificationWorkerConfig.LegacyEnabled { + queue.AddWorkers(handlers.NewNotificationWorker(notificationWorkerConfig, commands, q, c)) + } } func Start(ctx context.Context) { for _, projection := range projections { projection.Start(ctx) } - worker.Start(ctx) } func ProjectInstance(ctx context.Context) error { diff --git a/internal/notification/static/i18n/ro.yaml b/internal/notification/static/i18n/ro.yaml new file mode 100644 index 0000000000..f3c635a817 --- /dev/null +++ b/internal/notification/static/i18n/ro.yaml @@ -0,0 +1,68 @@ +InitCode: + Title: Inițializare Utilizator + PreHeader: Inițializare Utilizator + Subject: Inițializare Utilizator + Greeting: Bună ziua, {{.DisplayName}}, + Text: Acest utilizator a fost creat. Folosiți numele de utilizator {{.PreferredLoginName}} pentru a vă autentifica. Vă rugăm să dați clic pe butonul de mai jos pentru a finaliza procesul de inițializare. (Cod {{.Code}}) Dacă nu ați solicitat acest e-mail, vă rugăm să îl ignorați. + ButtonText: Finalizare inițializare +PasswordReset: + Title: Resetare parolă + PreHeader: Resetare parolă + Subject: Resetare parolă + Greeting: Bună ziua, {{.DisplayName}}, + Text: Am primit o cerere de resetare a parolei. Vă rugăm să folosiți butonul de mai jos pentru a vă reseta parola. (Cod {{.Code}}) Dacă nu ați solicitat acest e-mail, vă rugăm să îl ignorați. + ButtonText: Resetare parolă +VerifyEmail: + Title: Verificare e-mail + PreHeader: Verificare e-mail + Subject: Verificare e-mail + Greeting: Bună ziua, {{.DisplayName}}, + Text: A fost adăugat un e-mail nou. Vă rugăm să folosiți butonul de mai jos pentru a vă verifica e-mailul. (Cod {{.Code}}) Dacă nu ați adăugat un e-mail nou, vă rugăm să ignorați acest e-mail. + ButtonText: Verificare e-mail +VerifyPhone: + Title: Verificare telefon + PreHeader: Verificare telefon + Subject: Verificare telefon + Greeting: Bună ziua, {{.DisplayName}}, + Text: "A fost adăugat un număr de telefon nou. Vă rugăm să folosiți următorul cod pentru a-l verifica: {{.Code}}" + ButtonText: Verificare telefon +VerifyEmailOTP: + Title: Verificare parolă unică + PreHeader: Verificare parolă unică + Subject: Verificare parolă unică + Greeting: Bună ziua, {{.DisplayName}}, + Text: Vă rugăm să folosiți parola unică {{.OTP}} pentru a vă autentifica în următoarele cinci minute sau dați clic pe butonul "Autentificare". + ButtonText: Autentificare +VerifySMSOTP: + Text: >- + {{.OTP}} este parola dvs. unică pentru {{ .Domain }}. Folosiți-o în următoarele {{.Expiry}}. + + @{{.Domain}} #{{.OTP}} +DomainClaimed: + Title: Domeniul a fost revendicat + PreHeader: Schimbare e-mail / nume de utilizator + Subject: Domeniul a fost revendicat + Greeting: Bună ziua, {{.DisplayName}}, + Text: Domeniul {{.Domain}} a fost revendicat de o organizație. Utilizatorul dvs. actual {{.Username}} nu face parte din această organizație. Prin urmare, va trebui să vă schimbați e-mailul când vă autentificați. Am creat un nume de utilizator temporar ({{.TempUsername}}) pentru această autentificare. + ButtonText: Autentificare +PasswordlessRegistration: + Title: Adăugare autentificare fără parolă + PreHeader: Adăugare autentificare fără parolă + Subject: Adăugare autentificare fără parolă + Greeting: Bună ziua, {{.DisplayName}}, + Text: Am primit o cerere de adăugare a unui token pentru autentificare fără parolă. Vă rugăm să folosiți butonul de mai jos pentru a adăuga token-ul sau dispozitivul pentru autentificare fără parolă. + ButtonText: Adăugare autentificare fără parolă +PasswordChange: + Title: Parola utilizatorului a fost schimbată + PreHeader: Schimbare parolă + Subject: Parola utilizatorului a fost schimbată + Greeting: Bună ziua, {{.DisplayName}}, + Text: Parola utilizatorului dvs. a fost schimbată. Dacă această modificare nu a fost făcută de dvs., vă recomandăm să vă resetați imediat parola. + ButtonText: Autentificare +InviteUser: + Title: Invitație la {{.ApplicationName}} + PreHeader: Invitație la {{.ApplicationName}} + Subject: Invitație la {{.ApplicationName}} + Greeting: Bună ziua, {{.DisplayName}}, + Text: Utilizatorul dvs. a fost invitat la {{.ApplicationName}}. Vă rugăm să dați clic pe butonul de mai jos pentru a finaliza procesul de invitație. Dacă nu ați solicitat acest e-mail, vă rugăm să îl ignorați. + ButtonText: Acceptare invitație diff --git a/internal/notification/templates/template.go b/internal/notification/templates/template.go index 98209366c2..734d7052df 100644 --- a/internal/notification/templates/template.go +++ b/internal/notification/templates/template.go @@ -3,7 +3,7 @@ package templates import ( "bytes" "html/template" - "io/ioutil" + "io" "net/http" ) @@ -51,7 +51,7 @@ func readFile(dir http.FileSystem, fileName string) (*template.Template, error) return nil, err } defer f.Close() - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func readFileFromDatabase(dir http.FileSystem, fileName string) (*template.Templ return nil, err } defer f.Close() - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return nil, err } diff --git a/internal/notification/types/notification.go b/internal/notification/types/notification.go index db791851bc..7b6aff6010 100644 --- a/internal/notification/types/notification.go +++ b/internal/notification/types/notification.go @@ -39,7 +39,7 @@ func SendEmail( translator *i18n.Translator, user *query.NotifyUser, colors *query.LabelPolicy, - triggeringEvent eventstore.Event, + triggeringEventType eventstore.EventType, ) Notify { return func( urlTmpl string, @@ -66,7 +66,7 @@ func SendEmail( data, args, allowUnverifiedNotificationChannel, - triggeringEvent, + triggeringEventType, ) } } @@ -102,7 +102,9 @@ func SendSMS( translator *i18n.Translator, user *query.NotifyUser, colors *query.LabelPolicy, - triggeringEvent eventstore.Event, + triggeringEventType eventstore.EventType, + instanceID string, + jobID string, generatorInfo *senders.CodeGeneratorInfo, ) Notify { return func( @@ -124,7 +126,9 @@ func SendSMS( data, args, allowUnverifiedNotificationChannel, - triggeringEvent, + triggeringEventType, + instanceID, + jobID, generatorInfo, ) } @@ -135,7 +139,7 @@ func SendJSON( webhookConfig webhook.Config, channels ChannelChains, serializable interface{}, - triggeringEvent eventstore.Event, + triggeringEventType eventstore.EventType, ) Notify { return func(_ string, _ map[string]interface{}, _ string, _ bool) error { return handleWebhook( @@ -143,7 +147,7 @@ func SendJSON( webhookConfig, channels, serializable, - triggeringEvent, + triggeringEventType, ) } } @@ -153,7 +157,7 @@ func SendSecurityTokenEvent( setConfig set.Config, channels ChannelChains, token any, - triggeringEvent eventstore.Event, + triggeringEventType eventstore.EventType, ) Notify { return func(_ string, _ map[string]interface{}, _ string, _ bool) error { return handleSecurityTokenEvent( @@ -161,7 +165,7 @@ func SendSecurityTokenEvent( setConfig, channels, token, - triggeringEvent, + triggeringEventType, ) } } diff --git a/internal/notification/types/security_token_event.go b/internal/notification/types/security_token_event.go index d8a1d26006..34a7389975 100644 --- a/internal/notification/types/security_token_event.go +++ b/internal/notification/types/security_token_event.go @@ -13,11 +13,11 @@ func handleSecurityTokenEvent( setConfig set.Config, channels ChannelChains, token any, - triggeringEvent eventstore.Event, + triggeringEventType eventstore.EventType, ) error { message := &messages.Form{ - Serializable: token, - TriggeringEvent: triggeringEvent, + Serializable: token, + TriggeringEventType: triggeringEventType, } setChannels, err := channels.SecurityTokenEvent(ctx, setConfig) if err != nil { diff --git a/internal/notification/types/user_email.go b/internal/notification/types/user_email.go index d32ee868f0..626625b5ab 100644 --- a/internal/notification/types/user_email.go +++ b/internal/notification/types/user_email.go @@ -23,7 +23,7 @@ func generateEmail( data templates.TemplateData, args map[string]interface{}, lastEmail bool, - triggeringEvent eventstore.Event, + triggeringEventType eventstore.EventType, ) error { emailChannels, config, err := channels.Email(ctx) logging.OnError(err).Error("could not create email channel") @@ -38,10 +38,10 @@ func generateEmail( } if config.SMTPConfig != nil { message := &messages.Email{ - Recipients: []string{recipient}, - Subject: data.Subject, - Content: html.UnescapeString(template), - TriggeringEvent: triggeringEvent, + Recipients: []string{recipient}, + Subject: data.Subject, + Content: html.UnescapeString(template), + TriggeringEventType: triggeringEventType, } return emailChannels.HandleMessage(message) } @@ -52,7 +52,7 @@ func generateEmail( } contextInfo := map[string]interface{}{ "recipientEmailAddress": recipient, - "eventType": triggeringEvent.Type(), + "eventType": triggeringEventType, "provider": config.ProviderConfig, } @@ -62,7 +62,7 @@ func generateEmail( TemplateData: data, Args: caseArgs, }, - TriggeringEvent: triggeringEvent, + TriggeringEventType: triggeringEventType, } webhookChannels, err := channels.Webhook(ctx, *config.WebhookConfig) if err != nil { diff --git a/internal/notification/types/user_phone.go b/internal/notification/types/user_phone.go index 3ee202dfab..d0709a6cc0 100644 --- a/internal/notification/types/user_phone.go +++ b/internal/notification/types/user_phone.go @@ -28,7 +28,9 @@ func generateSms( data templates.TemplateData, args map[string]interface{}, lastPhone bool, - triggeringEvent eventstore.Event, + triggeringEventType eventstore.EventType, + instanceID string, + jobID string, generatorInfo *senders.CodeGeneratorInfo, ) error { smsChannels, config, err := channels.SMS(ctx) @@ -51,7 +53,10 @@ func generateSms( SenderPhoneNumber: number, RecipientPhoneNumber: recipient, Content: data.Text, - TriggeringEvent: triggeringEvent, + TriggeringEventType: triggeringEventType, + InstanceID: instanceID, + JobID: jobID, + UserID: user.ID, } err = smsChannels.HandleMessage(message) if err != nil { @@ -70,7 +75,7 @@ func generateSms( } contextInfo := map[string]interface{}{ "recipientPhoneNumber": recipient, - "eventType": triggeringEvent.Type(), + "eventType": triggeringEventType, "provider": config.ProviderConfig, } @@ -80,7 +85,7 @@ func generateSms( Args: caseArgs, ContextInfo: contextInfo, }, - TriggeringEvent: triggeringEvent, + TriggeringEventType: triggeringEventType, } webhookChannels, err := channels.Webhook(ctx, *config.WebhookConfig) if err != nil { diff --git a/internal/notification/types/webhook.go b/internal/notification/types/webhook.go index 465be289d1..3ffefd7e26 100644 --- a/internal/notification/types/webhook.go +++ b/internal/notification/types/webhook.go @@ -13,11 +13,11 @@ func handleWebhook( webhookConfig webhook.Config, channels ChannelChains, serializable interface{}, - triggeringEvent eventstore.Event, + triggeringEventType eventstore.EventType, ) error { message := &messages.JSON{ - Serializable: serializable, - TriggeringEvent: triggeringEvent, + Serializable: serializable, + TriggeringEventType: triggeringEventType, } webhookChannels, err := channels.Webhook(ctx, webhookConfig) if err != nil { diff --git a/internal/query/action.go b/internal/query/action.go index 30ded403d1..45017572e2 100644 --- a/internal/query/action.go +++ b/internal/query/action.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -118,7 +117,7 @@ func (q *Queries) SearchActions(ctx context.Context, queries *ActionSearchQuerie ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareActionsQuery(ctx, q.client) + query, scan := prepareActionsQuery() eq := sq.Eq{ ActionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -146,7 +145,7 @@ func (q *Queries) GetActionByID(ctx context.Context, id string, orgID string, wi ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareActionQuery(ctx, q.client) + stmt, scan := prepareActionQuery() eq := sq.Eq{ ActionColumnID.identifier(): id, ActionColumnResourceOwner.identifier(): orgID, @@ -183,7 +182,7 @@ func NewActionIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(ActionColumnID, id, TextEquals) } -func prepareActionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Actions, error)) { +func prepareActionsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Actions, error)) { return sq.Select( ActionColumnID.identifier(), ActionColumnCreationDate.identifier(), @@ -196,7 +195,7 @@ func prepareActionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil ActionColumnTimeout.identifier(), ActionColumnAllowedToFail.identifier(), countColumn.identifier(), - ).From(actionTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(actionTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Actions, error) { actions := make([]*Action, 0) @@ -235,7 +234,7 @@ func prepareActionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareActionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Action, error)) { +func prepareActionQuery() (sq.SelectBuilder, func(row *sql.Row) (*Action, error)) { return sq.Select( ActionColumnID.identifier(), ActionColumnCreationDate.identifier(), @@ -247,7 +246,7 @@ func prepareActionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild ActionColumnScript.identifier(), ActionColumnTimeout.identifier(), ActionColumnAllowedToFail.identifier(), - ).From(actionTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(actionTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Action, error) { action := new(Action) diff --git a/internal/query/action_flow.go b/internal/query/action_flow.go index c5263d6c43..6011b3f6e1 100644 --- a/internal/query/action_flow.go +++ b/internal/query/action_flow.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -67,7 +66,7 @@ func (q *Queries) GetFlow(ctx context.Context, flowType domain.FlowType, orgID s ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareFlowQuery(ctx, q.client, flowType) + query, scan := prepareFlowQuery(flowType) eq := sq.Eq{ FlowsTriggersColumnFlowType.identifier(): flowType, FlowsTriggersColumnResourceOwner.identifier(): orgID, @@ -89,7 +88,7 @@ func (q *Queries) GetActiveActionsByFlowAndTriggerType(ctx context.Context, flow ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareTriggerActionsQuery(ctx, q.client) + stmt, scan := prepareTriggerActionsQuery() eq := sq.Eq{ FlowsTriggersColumnFlowType.identifier(): flowType, FlowsTriggersColumnTriggerType.identifier(): triggerType, @@ -113,7 +112,7 @@ func (q *Queries) GetFlowTypesOfActionID(ctx context.Context, actionID string) ( ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareFlowTypesQuery(ctx, q.client) + stmt, scan := prepareFlowTypesQuery() eq := sq.Eq{ FlowsTriggersColumnActionID.identifier(): actionID, FlowsTriggersColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -130,11 +129,11 @@ func (q *Queries) GetFlowTypesOfActionID(ctx context.Context, actionID string) ( return types, err } -func prepareFlowTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]domain.FlowType, error)) { +func prepareFlowTypesQuery() (sq.SelectBuilder, func(*sql.Rows) ([]domain.FlowType, error)) { return sq.Select( FlowsTriggersColumnFlowType.identifier(), ). - From(flowsTriggersTable.identifier() + db.Timetravel(call.Took(ctx))). + From(flowsTriggersTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) ([]domain.FlowType, error) { types := []domain.FlowType{} @@ -153,7 +152,7 @@ func prepareFlowTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } -func prepareTriggerActionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]*Action, error)) { +func prepareTriggerActionsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*Action, error)) { return sq.Select( ActionColumnID.identifier(), ActionColumnCreationDate.identifier(), @@ -167,7 +166,7 @@ func prepareTriggerActionsQuery(ctx context.Context, db prepareDatabase) (sq.Sel ActionColumnTimeout.identifier(), ). From(flowsTriggersTable.name). - LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID)). OrderBy(FlowsTriggersColumnTriggerSequence.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) ([]*Action, error) { @@ -200,7 +199,7 @@ func prepareTriggerActionsQuery(ctx context.Context, db prepareDatabase) (sq.Sel } } -func prepareFlowQuery(ctx context.Context, db prepareDatabase, flowType domain.FlowType) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { +func prepareFlowQuery(flowType domain.FlowType) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { return sq.Select( ActionColumnID.identifier(), ActionColumnCreationDate.identifier(), @@ -220,7 +219,7 @@ func prepareFlowQuery(ctx context.Context, db prepareDatabase, flowType domain.F FlowsTriggersColumnResourceOwner.identifier(), ). From(flowsTriggersTable.name). - LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID)). OrderBy(FlowsTriggersColumnTriggerSequence.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Flow, error) { diff --git a/internal/query/action_flow_test.go b/internal/query/action_flow_test.go index af0db27278..7447313064 100644 --- a/internal/query/action_flow_test.go +++ b/internal/query/action_flow_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -34,7 +33,6 @@ var ( ` projections.flow_triggers3.resource_owner` + ` FROM projections.flow_triggers3` + ` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ORDER BY projections.flow_triggers3.trigger_sequence` prepareFlowCols = []string{ "id", @@ -68,7 +66,6 @@ var ( ` projections.actions3.timeout` + ` FROM projections.flow_triggers3` + ` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ORDER BY projections.flow_triggers3.trigger_sequence` prepareTriggerActionCols = []string{ @@ -86,7 +83,6 @@ var ( prepareFlowTypeStmt = `SELECT projections.flow_triggers3.flow_type` + ` FROM projections.flow_triggers3` - // ` AS OF SYSTEM TIME '-1 ms'` prepareFlowTypeCols = []string{ "flow_type", @@ -106,8 +102,8 @@ func Test_FlowPrepares(t *testing.T) { }{ { name: "prepareFlowQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueries( @@ -123,8 +119,8 @@ func Test_FlowPrepares(t *testing.T) { }, { name: "prepareFlowQuery one action", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueries( @@ -177,8 +173,8 @@ func Test_FlowPrepares(t *testing.T) { }, { name: "prepareFlowQuery multiple actions", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueries( @@ -263,8 +259,8 @@ func Test_FlowPrepares(t *testing.T) { }, { name: "prepareFlowQuery no action", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueries( @@ -302,8 +298,8 @@ func Test_FlowPrepares(t *testing.T) { }, { name: "prepareFlowQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { - return prepareFlowQuery(ctx, db, domain.FlowTypeExternalAuthentication) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { + return prepareFlowQuery(domain.FlowTypeExternalAuthentication) }, want: want{ sqlExpectations: mockQueryErr( @@ -520,7 +516,7 @@ func Test_FlowPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/action_test.go b/internal/query/action_test.go index f6ba5be4b9..e5cad0e269 100644 --- a/internal/query/action_test.go +++ b/internal/query/action_test.go @@ -26,7 +26,6 @@ var ( ` projections.actions3.allowed_to_fail,` + ` COUNT(*) OVER ()` + ` FROM projections.actions3` - // ` AS OF SYSTEM TIME '-1 ms'` prepareActionsCols = []string{ "id", "creation_date", @@ -52,7 +51,6 @@ var ( ` projections.actions3.timeout,` + ` projections.actions3.allowed_to_fail` + ` FROM projections.actions3` - // ` AS OF SYSTEM TIME '-1 ms'` prepareActionCols = []string{ "id", "creation_date", @@ -289,7 +287,7 @@ func Test_ActionPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/app.go b/internal/query/app.go index 1aa0323a5a..5fed1e3ced 100644 --- a/internal/query/app.go +++ b/internal/query/app.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -66,9 +65,11 @@ type OIDCApp struct { } type SAMLApp struct { - Metadata []byte - MetadataURL string - EntityID string + Metadata []byte + MetadataURL string + EntityID string + LoginVersion domain.LoginVersion + LoginBaseURI *string } type APIApp struct { @@ -137,6 +138,10 @@ var ( name: projection.AppSAMLTable, instanceIDCol: projection.AppSAMLConfigColumnInstanceID, } + AppSAMLConfigColumnInstanceID = Column{ + name: projection.AppSAMLConfigColumnInstanceID, + table: appSAMLConfigsTable, + } AppSAMLConfigColumnAppID = Column{ name: projection.AppSAMLConfigColumnAppID, table: appSAMLConfigsTable, @@ -153,6 +158,14 @@ var ( name: projection.AppSAMLConfigColumnMetadataURL, table: appSAMLConfigsTable, } + AppSAMLConfigColumnLoginVersion = Column{ + name: projection.AppSAMLConfigColumnLoginVersion, + table: appSAMLConfigsTable, + } + AppSAMLConfigColumnLoginBaseURI = Column{ + name: projection.AppSAMLConfigColumnLoginBaseURI, + table: appSAMLConfigsTable, + } ) var ( @@ -276,7 +289,7 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo traceSpan.EndWithError(err) } - stmt, scan := prepareAppQuery(ctx, q.client, false) + stmt, scan := prepareAppQuery(false) eq := sq.Eq{ AppColumnID.identifier(): appID, AppColumnProjectID.identifier(): projectID, @@ -298,7 +311,7 @@ func (q *Queries) AppByID(ctx context.Context, appID string, activeOnly bool) (a ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareAppQuery(ctx, q.client, activeOnly) + stmt, scan := prepareAppQuery(activeOnly) eq := sq.Eq{ AppColumnID.identifier(): appID, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -320,35 +333,11 @@ func (q *Queries) AppByID(ctx context.Context, appID string, activeOnly bool) (a return app, err } -func (q *Queries) ActiveAppBySAMLEntityID(ctx context.Context, entityID string) (app *App, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - stmt, scan := prepareSAMLAppQuery(ctx, q.client) - eq := sq.Eq{ - AppSAMLConfigColumnEntityID.identifier(): entityID, - AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - AppColumnState.identifier(): domain.AppStateActive, - ProjectColumnState.identifier(): domain.ProjectStateActive, - OrgColumnState.identifier(): domain.OrgStateActive, - } - query, args, err := stmt.Where(eq).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-JgUop", "Errors.Query.SQLStatement") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - app, err = scan(row) - return err - }, query, args...) - return app, err -} - func (q *Queries) ProjectByClientID(ctx context.Context, appID string) (project *Project, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareProjectByAppQuery(ctx, q.client) + stmt, scan := prepareProjectByAppQuery() eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} query, args, err := stmt.Where(sq.And{ eq, @@ -444,7 +433,7 @@ func (q *Queries) ProjectIDFromClientID(ctx context.Context, appID string) (id s ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareProjectIDByAppQuery(ctx, q.client) + stmt, scan := prepareProjectIDByAppQuery() eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} where := sq.And{ eq, @@ -470,7 +459,7 @@ func (q *Queries) ProjectByOIDCClientID(ctx context.Context, id string) (project ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareProjectByOIDCAppQuery(ctx, q.client) + stmt, scan := prepareProjectByOIDCAppQuery() eq := sq.Eq{ AppOIDCConfigColumnClientID.identifier(): id, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -512,7 +501,7 @@ func (q *Queries) AppByClientID(ctx context.Context, clientID string) (app *App, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareAppQuery(ctx, q.client, true) + stmt, scan := prepareAppQuery(true) eq := sq.Eq{ AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), AppColumnState.identifier(): domain.AppStateActive, @@ -541,7 +530,7 @@ func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, wit ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareAppsQuery(ctx, q.client) + query, scan := prepareAppsQuery() eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -570,7 +559,7 @@ func (q *Queries) SearchClientIDs(ctx context.Context, queries *AppSearchQueries traceSpan.EndWithError(err) } - query, scan := prepareClientIDsQuery(ctx, q.client) + query, scan := prepareClientIDsQuery() eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -591,7 +580,7 @@ func (q *Queries) OIDCClientLoginVersion(ctx context.Context, clientID string) ( ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginVersionByClientID(ctx, q.client) + query, scan := prepareLoginVersionByOIDCClientID() eq := sq.Eq{ AppOIDCConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), AppOIDCConfigColumnClientID.identifier(): clientID, @@ -611,6 +600,30 @@ func (q *Queries) OIDCClientLoginVersion(ctx context.Context, clientID string) ( return loginVersion, nil } +func (q *Queries) SAMLAppLoginVersion(ctx context.Context, appID string) (loginVersion domain.LoginVersion, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, scan := prepareLoginVersionBySAMLAppID() + eq := sq.Eq{ + AppSAMLConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + AppSAMLConfigColumnAppID.identifier(): appID, + } + stmt, args, err := query.Where(eq).ToSql() + if err != nil { + return domain.LoginVersionUnspecified, zerrors.ThrowInvalidArgument(err, "QUERY-TnaciwZfp3", "Errors.Query.InvalidRequest") + } + + err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { + loginVersion, err = scan(row) + return err + }, stmt, args...) + if err != nil { + return domain.LoginVersionUnspecified, zerrors.ThrowInternal(err, "QUERY-lvDDwRzIoP", "Errors.Internal") + } + return loginVersion, nil +} + func NewAppNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { return NewTextQuery(AppColumnName, value, method) } @@ -619,7 +632,7 @@ func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(AppColumnProjectID, id, TextEquals) } -func prepareAppQuery(ctx context.Context, db prepareDatabase, activeOnly bool) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { +func prepareAppQuery(activeOnly bool) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { query := sq.Select( AppColumnID.identifier(), AppColumnName.identifier(), @@ -659,6 +672,8 @@ func prepareAppQuery(ctx context.Context, db prepareDatabase, activeOnly bool) ( AppSAMLConfigColumnEntityID.identifier(), AppSAMLConfigColumnMetadata.identifier(), AppSAMLConfigColumnMetadataURL.identifier(), + AppSAMLConfigColumnLoginVersion.identifier(), + AppSAMLConfigColumnLoginBaseURI.identifier(), ).From(appsTable.identifier()). PlaceholderFormat(sq.Dollar) @@ -668,13 +683,13 @@ func prepareAppQuery(ctx context.Context, db prepareDatabase, activeOnly bool) ( LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). LeftJoin(join(ProjectColumnID, AppColumnProjectID)). - LeftJoin(join(OrgColumnID, AppColumnResourceOwner) + db.Timetravel(call.Took(ctx))), + LeftJoin(join(OrgColumnID, AppColumnResourceOwner)), scanApp } return query. LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))), + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)), scanApp } @@ -726,6 +741,8 @@ func scanApp(row *sql.Row) (*App, error) { &samlConfig.entityID, &samlConfig.metadata, &samlConfig.metadataURL, + &samlConfig.loginVersion, + &samlConfig.loginBaseURI, ) if err != nil { @@ -827,68 +844,13 @@ func prepareOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { } } -func prepareSAMLAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return sq.Select( - AppColumnID.identifier(), - AppColumnName.identifier(), - AppColumnProjectID.identifier(), - AppColumnCreationDate.identifier(), - AppColumnChangeDate.identifier(), - AppColumnResourceOwner.identifier(), - AppColumnState.identifier(), - AppColumnSequence.identifier(), - - AppSAMLConfigColumnAppID.identifier(), - AppSAMLConfigColumnEntityID.identifier(), - AppSAMLConfigColumnMetadata.identifier(), - AppSAMLConfigColumnMetadataURL.identifier(), - ).From(appsTable.identifier()). - Join(join(AppSAMLConfigColumnAppID, AppColumnID)). - Join(join(ProjectColumnID, AppColumnProjectID)). - Join(join(OrgColumnID, AppColumnResourceOwner)). - PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*App, error) { - - app := new(App) - var ( - samlConfig = sqlSAMLConfig{} - ) - - err := row.Scan( - &app.ID, - &app.Name, - &app.ProjectID, - &app.CreationDate, - &app.ChangeDate, - &app.ResourceOwner, - &app.State, - &app.Sequence, - - &samlConfig.appID, - &samlConfig.entityID, - &samlConfig.metadata, - &samlConfig.metadataURL, - ) - - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-d6TO1", "Errors.App.NotExisting") - } - return nil, zerrors.ThrowInternal(err, "QUERY-NAtPg", "Errors.Internal") - } - - samlConfig.set(app) - - return app, nil - } -} - -func prepareProjectIDByAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (projectID string, err error)) { +func prepareProjectIDByAppQuery() (sq.SelectBuilder, func(*sql.Row) (projectID string, err error)) { return sq.Select( AppColumnProjectID.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (projectID string, err error) { err = row.Scan( &projectID, @@ -905,7 +867,7 @@ func prepareProjectIDByAppQuery(ctx context.Context, db prepareDatabase) (sq.Sel } } -func prepareProjectByOIDCAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { +func prepareProjectByOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), @@ -947,7 +909,7 @@ func prepareProjectByOIDCAppQuery(ctx context.Context, db prepareDatabase) (sq.S } } -func prepareProjectByAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { +func prepareProjectByAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), @@ -964,7 +926,7 @@ func prepareProjectByAppQuery(ctx context.Context, db prepareDatabase) (sq.Selec Join(join(AppColumnProjectID, ProjectColumnID)). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Project, error) { p := new(Project) @@ -991,7 +953,7 @@ func prepareProjectByAppQuery(ctx context.Context, db prepareDatabase) (sq.Selec } } -func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) { +func prepareAppsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) { return sq.Select( AppColumnID.identifier(), AppColumnName.identifier(), @@ -1031,11 +993,13 @@ func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder AppSAMLConfigColumnEntityID.identifier(), AppSAMLConfigColumnMetadata.identifier(), AppSAMLConfigColumnMetadataURL.identifier(), + AppSAMLConfigColumnLoginVersion.identifier(), + AppSAMLConfigColumnLoginBaseURI.identifier(), countColumn.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Rows) (*Apps, error) { apps := &Apps{Apps: []*App{}} @@ -1086,6 +1050,8 @@ func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder &samlConfig.entityID, &samlConfig.metadata, &samlConfig.metadataURL, + &samlConfig.loginVersion, + &samlConfig.loginBaseURI, &apps.Count, ) @@ -1105,13 +1071,13 @@ func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder } } -func prepareClientIDsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]string, error)) { +func prepareClientIDsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]string, error)) { return sq.Select( AppAPIConfigColumnClientID.identifier(), AppOIDCConfigColumnClientID.identifier(), ).From(appsTable.identifier()). LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) ([]string, error) { ids := database.TextArray[string]{} @@ -1135,7 +1101,7 @@ func prepareClientIDsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareLoginVersionByClientID(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (domain.LoginVersion, error)) { +func prepareLoginVersionByOIDCClientID() (sq.SelectBuilder, func(*sql.Row) (domain.LoginVersion, error)) { return sq.Select( AppOIDCConfigColumnLoginVersion.identifier(), ).From(appOIDCConfigsTable.identifier()). @@ -1150,6 +1116,21 @@ func prepareLoginVersionByClientID(ctx context.Context, db prepareDatabase) (sq. } } +func prepareLoginVersionBySAMLAppID() (sq.SelectBuilder, func(*sql.Row) (domain.LoginVersion, error)) { + return sq.Select( + AppSAMLConfigColumnLoginVersion.identifier(), + ).From(appSAMLConfigsTable.identifier()). + PlaceholderFormat(sq.Dollar), func(row *sql.Row) (domain.LoginVersion, error) { + var loginVersion sql.NullInt16 + if err := row.Scan( + &loginVersion, + ); err != nil { + return domain.LoginVersionUnspecified, zerrors.ThrowInternal(err, "QUERY-KbzaCnaziI", "Errors.Internal") + } + return domain.LoginVersion(loginVersion.Int16), nil + } +} + type sqlOIDCConfig struct { appID sql.NullString version sql.NullInt32 @@ -1209,10 +1190,12 @@ func (c sqlOIDCConfig) set(app *App) { } type sqlSAMLConfig struct { - appID sql.NullString - entityID sql.NullString - metadataURL sql.NullString - metadata []byte + appID sql.NullString + entityID sql.NullString + metadataURL sql.NullString + metadata []byte + loginVersion sql.NullInt16 + loginBaseURI sql.NullString } func (c sqlSAMLConfig) set(app *App) { @@ -1220,9 +1203,13 @@ func (c sqlSAMLConfig) set(app *App) { return } app.SAMLConfig = &SAMLApp{ - MetadataURL: c.metadataURL.String, - Metadata: c.metadata, - EntityID: c.entityID.String, + EntityID: c.entityID.String, + MetadataURL: c.metadataURL.String, + Metadata: c.metadata, + LoginVersion: domain.LoginVersion(c.loginVersion.Int16), + } + if c.loginBaseURI.Valid { + app.SAMLConfig.LoginBaseURI = &c.loginBaseURI.String } } diff --git a/internal/query/app_test.go b/internal/query/app_test.go index ea9444f665..c24060a60c 100644 --- a/internal/query/app_test.go +++ b/internal/query/app_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -56,7 +55,9 @@ var ( ` projections.apps7_saml_configs.app_id,` + ` projections.apps7_saml_configs.entity_id,` + ` projections.apps7_saml_configs.metadata,` + - ` projections.apps7_saml_configs.metadata_url` + + ` projections.apps7_saml_configs.metadata_url,` + + ` projections.apps7_saml_configs.login_version,` + + ` projections.apps7_saml_configs.login_base_uri` + ` FROM projections.apps7` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + @@ -103,24 +104,23 @@ var ( ` projections.apps7_saml_configs.entity_id,` + ` projections.apps7_saml_configs.metadata,` + ` projections.apps7_saml_configs.metadata_url,` + + ` projections.apps7_saml_configs.login_version,` + + ` projections.apps7_saml_configs.login_base_uri,` + ` COUNT(*) OVER ()` + ` FROM projections.apps7` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id`) expectedAppIDsQuery = regexp.QuoteMeta(`SELECT projections.apps7_api_configs.client_id,` + ` projections.apps7_oidc_configs.client_id` + ` FROM projections.apps7` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + - ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id`) expectedProjectIDByAppQuery = regexp.QuoteMeta(`SELECT projections.apps7.project_id` + ` FROM projections.apps7` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id`) expectedProjectByAppQuery = regexp.QuoteMeta(`SELECT projections.projects4.id,` + ` projections.projects4.creation_date,` + ` projections.projects4.change_date,` + @@ -136,8 +136,7 @@ var ( ` JOIN projections.apps7 ON projections.projects4.id = projections.apps7.project_id AND projections.projects4.instance_id = projections.apps7.instance_id` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id`) appCols = database.TextArray[string]{ "id", @@ -178,6 +177,8 @@ var ( "entity_id", "metadata", "metadata_url", + "login_version", + "login_base_uri", } appsCols = append(appCols, "count") ) @@ -252,6 +253,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -321,6 +324,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -393,6 +398,8 @@ func Test_AppsPrepare(t *testing.T) { "https://test.com/saml/metadata", []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), "https://test.com/saml/metadata", + domain.LoginVersionUnspecified, + nil, }, }, ), @@ -467,6 +474,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -559,6 +568,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -651,6 +662,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -743,6 +756,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -835,6 +850,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -927,6 +944,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -1019,6 +1038,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, { "api-app-id", @@ -1059,6 +1080,8 @@ func Test_AppsPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, { "saml-app-id", @@ -1099,6 +1122,8 @@ func Test_AppsPrepare(t *testing.T) { "https://test.com/saml/metadata", []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), "https://test.com/saml/metadata", + domain.LoginVersion2, + "https://login.ch/", }, }, ), @@ -1165,9 +1190,11 @@ func Test_AppsPrepare(t *testing.T) { Name: "app-name", ProjectID: "project-id", SAMLConfig: &SAMLApp{ - Metadata: []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), - MetadataURL: "https://test.com/saml/metadata", - EntityID: "https://test.com/saml/metadata", + Metadata: []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + MetadataURL: "https://test.com/saml/metadata", + EntityID: "https://test.com/saml/metadata", + LoginVersion: domain.LoginVersion2, + LoginBaseURI: gu.Ptr("https://login.ch/"), }, }, }, @@ -1196,7 +1223,7 @@ func Test_AppsPrepare(t *testing.T) { if tt.name == "prepareAppsQuery oidc app" { _ = tt.name } - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -1214,8 +1241,8 @@ func Test_AppPrepare(t *testing.T) { }{ { name: "prepareAppQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueriesScanErr( @@ -1234,8 +1261,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery found", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQuery( @@ -1280,6 +1307,8 @@ func Test_AppPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, ), }, @@ -1296,8 +1325,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery api app", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1343,6 +1372,8 @@ func Test_AppPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -1364,8 +1395,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1411,6 +1442,8 @@ func Test_AppPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -1451,8 +1484,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app active only", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, true) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(true) }, want: want{ sqlExpectations: mockQueries( @@ -1498,6 +1531,8 @@ func Test_AppPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -1538,8 +1573,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery saml app", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1585,6 +1620,8 @@ func Test_AppPrepare(t *testing.T) { "https://test.com/saml/metadata", []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), "https://test.com/saml/metadata", + domain.LoginVersionUnspecified, + nil, }, }, ), @@ -1599,16 +1636,18 @@ func Test_AppPrepare(t *testing.T) { Name: "app-name", ProjectID: "project-id", SAMLConfig: &SAMLApp{ - Metadata: []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), - MetadataURL: "https://test.com/saml/metadata", - EntityID: "https://test.com/saml/metadata", + Metadata: []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + MetadataURL: "https://test.com/saml/metadata", + EntityID: "https://test.com/saml/metadata", + LoginVersion: domain.LoginVersionUnspecified, + LoginBaseURI: nil, }, }, }, { name: "prepareAppQuery oidc app IsDevMode inactive", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1654,6 +1693,8 @@ func Test_AppPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -1694,8 +1735,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app AssertAccessTokenRole inactive", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1741,6 +1782,8 @@ func Test_AppPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -1781,8 +1824,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app AssertIDTokenRole inactive", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1828,6 +1871,8 @@ func Test_AppPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -1868,8 +1913,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery oidc app AssertIDTokenUserinfo inactive", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueries( @@ -1915,6 +1960,8 @@ func Test_AppPrepare(t *testing.T) { nil, nil, nil, + nil, + nil, }, }, ), @@ -1955,8 +2002,8 @@ func Test_AppPrepare(t *testing.T) { }, { name: "prepareAppQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return prepareAppQuery(ctx, db, false) + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(false) }, want: want{ sqlExpectations: mockQueryErr( @@ -1975,7 +2022,7 @@ func Test_AppPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -2061,7 +2108,7 @@ func Test_AppIDsPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -2127,7 +2174,7 @@ func Test_ProjectIDByAppPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -2325,7 +2372,7 @@ func Test_ProjectByAppPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/auth_request.go b/internal/query/auth_request.go index 20ac0f5abd..eaf5e52491 100644 --- a/internal/query/auth_request.go +++ b/internal/query/auth_request.go @@ -5,13 +5,11 @@ import ( "database/sql" _ "embed" "errors" - "fmt" "time" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -44,10 +42,6 @@ func (a *AuthRequest) checkLoginClient(ctx context.Context, permissionCheck doma //go:embed auth_request_by_id.sql var authRequestByIDQuery string -func (q *Queries) authRequestByIDQuery(ctx context.Context) string { - return fmt.Sprintf(authRequestByIDQuery, q.client.Timetravel(call.Took(ctx))) -} - func (q *Queries) AuthRequestByID(ctx context.Context, shouldTriggerBulk bool, id string, checkLoginClient bool) (_ *AuthRequest, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -74,7 +68,7 @@ func (q *Queries) AuthRequestByID(ctx context.Context, shouldTriggerBulk bool, i &prompt, &locales, &dst.LoginHint, &dst.MaxAge, &dst.HintUserID, ) }, - q.authRequestByIDQuery(ctx), + authRequestByIDQuery, id, authz.GetInstance(ctx).InstanceID(), ) if errors.Is(err, sql.ErrNoRows) { diff --git a/internal/query/auth_request_by_id.sql b/internal/query/auth_request_by_id.sql index ffc18fccd6..f842719d0e 100644 --- a/internal/query/auth_request_by_id.sql +++ b/internal/query/auth_request_by_id.sql @@ -10,6 +10,6 @@ select login_hint, max_age, hint_user_id -from projections.auth_requests %s +from projections.auth_requests where id = $1 and instance_id = $2 limit 1; diff --git a/internal/query/auth_request_test.go b/internal/query/auth_request_test.go index 479282f9f7..152a032cd8 100644 --- a/internal/query/auth_request_test.go +++ b/internal/query/auth_request_test.go @@ -24,7 +24,6 @@ import ( func TestQueries_AuthRequestByID(t *testing.T) { expQuery := regexp.QuoteMeta(fmt.Sprintf( authRequestByIDQuery, - asOfSystemTime, )) cols := []string{ @@ -207,8 +206,7 @@ func TestQueries_AuthRequestByID(t *testing.T) { execMock(t, tt.expect, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, checkPermission: tt.permissionCheck, } diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index 6c05a03f6f..8075422e63 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -129,7 +128,7 @@ func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQu ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareAuthNKeysQuery(ctx, q.client) + query, scan := prepareAuthNKeysQuery() query = queries.toQuery(query) eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, @@ -156,7 +155,7 @@ func (q *Queries) SearchAuthNKeysData(ctx context.Context, queries *AuthNKeySear ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareAuthNKeysDataQuery(ctx, q.client) + query, scan := prepareAuthNKeysDataQuery() query = queries.toQuery(query) eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, @@ -189,7 +188,7 @@ func (q *Queries) GetAuthNKeyByID(ctx context.Context, shouldTriggerBulk bool, i traceSpan.EndWithError(err) } - query, scan := prepareAuthNKeyQuery(ctx, q.client) + query, scan := prepareAuthNKeyQuery() for _, q := range queries { query = q.toQuery(query) } @@ -214,7 +213,7 @@ func (q *Queries) GetAuthNKeyPublicKeyByIDAndIdentifier(ctx context.Context, id ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareAuthNKeyPublicKeyQuery(ctx, q.client) + stmt, scan := prepareAuthNKeyPublicKeyQuery() eq := sq.And{ sq.Eq{ AuthNKeyColumnID.identifier(): id, @@ -288,7 +287,7 @@ func (q *Queries) GetAuthNKeyUser(ctx context.Context, keyID, userID string) (_ return dst, nil } -func prepareAuthNKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { +func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { return sq.Select( AuthNKeyColumnID.identifier(), AuthNKeyColumnCreationDate.identifier(), @@ -298,7 +297,7 @@ func prepareAuthNKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu AuthNKeyColumnExpiration.identifier(), AuthNKeyColumnType.identifier(), countColumn.identifier(), - ).From(authNKeyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(authNKeyTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*AuthNKeys, error) { authNKeys := make([]*AuthNKey, 0) @@ -334,7 +333,7 @@ func prepareAuthNKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareAuthNKeyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) { +func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) { return sq.Select( AuthNKeyColumnID.identifier(), AuthNKeyColumnCreationDate.identifier(), @@ -343,7 +342,7 @@ func prepareAuthNKeyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui AuthNKeyColumnSequence.identifier(), AuthNKeyColumnExpiration.identifier(), AuthNKeyColumnType.identifier(), - ).From(authNKeyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(authNKeyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*AuthNKey, error) { authNKey := new(AuthNKey) @@ -366,10 +365,10 @@ func prepareAuthNKeyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui } } -func prepareAuthNKeyPublicKeyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) ([]byte, error)) { +func prepareAuthNKeyPublicKeyQuery() (sq.SelectBuilder, func(row *sql.Row) ([]byte, error)) { return sq.Select( AuthNKeyColumnPublicKey.identifier(), - ).From(authNKeyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(authNKeyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) ([]byte, error) { var publicKey []byte @@ -386,7 +385,7 @@ func prepareAuthNKeyPublicKeyQuery(ctx context.Context, db prepareDatabase) (sq. } } -func prepareAuthNKeysDataQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeysData, error)) { +func prepareAuthNKeysDataQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeysData, error)) { return sq.Select( AuthNKeyColumnID.identifier(), AuthNKeyColumnCreationDate.identifier(), @@ -398,7 +397,7 @@ func prepareAuthNKeysDataQuery(ctx context.Context, db prepareDatabase) (sq.Sele AuthNKeyColumnIdentifier.identifier(), AuthNKeyColumnPublicKey.identifier(), countColumn.identifier(), - ).From(authNKeyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(authNKeyTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*AuthNKeysData, error) { authNKeys := make([]*AuthNKeyData, 0) diff --git a/internal/query/authn_key_test.go b/internal/query/authn_key_test.go index 19005893f8..c7441f8dae 100644 --- a/internal/query/authn_key_test.go +++ b/internal/query/authn_key_test.go @@ -26,8 +26,7 @@ var ( ` projections.authn_keys2.expiration,` + ` projections.authn_keys2.type,` + ` COUNT(*) OVER ()` + - ` FROM projections.authn_keys2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.authn_keys2` prepareAuthNKeysCols = []string{ "id", "creation_date", @@ -49,8 +48,7 @@ var ( ` projections.authn_keys2.identifier,` + ` projections.authn_keys2.public_key,` + ` COUNT(*) OVER ()` + - ` FROM projections.authn_keys2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.authn_keys2` prepareAuthNKeysDataCols = []string{ "id", "creation_date", @@ -71,8 +69,7 @@ var ( ` projections.authn_keys2.sequence,` + ` projections.authn_keys2.expiration,` + ` projections.authn_keys2.type` + - ` FROM projections.authn_keys2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.authn_keys2` prepareAuthNKeyCols = []string{ "id", "creation_date", @@ -84,8 +81,7 @@ var ( } prepareAuthNKeyPublicKeyStmt = `SELECT projections.authn_keys2.public_key` + - ` FROM projections.authn_keys2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.authn_keys2` prepareAuthNKeyPublicKeyCols = []string{ "public_key", } @@ -471,7 +467,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -525,8 +521,7 @@ low2kyJov38V4Uk2I8kuXpLcnrpw5Tio2ooiUE27b0vHZqBKOei9Uo88qCrn3EKx execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "userID") diff --git a/internal/query/authn_key_user.sql b/internal/query/authn_key_user.sql index 5b7eaca63a..80de7a5167 100644 --- a/internal/query/authn_key_user.sql +++ b/internal/query/authn_key_user.sql @@ -3,9 +3,10 @@ from projections.authn_keys2 k join projections.users14 u on k.instance_id = u.instance_id and k.identifier = u.id -join projections.users14_machines m +join projections.users14_machines m on u.instance_id = m.instance_id and u.id = m.user_id where k.instance_id = $1 and k.id = $2 - and u.id = $3; + and u.id = $3 + and k.expiration > current_timestamp; diff --git a/internal/query/certificate.go b/internal/query/certificate.go index e4d53213cf..ebe4b249f4 100644 --- a/internal/query/certificate.go +++ b/internal/query/certificate.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -69,7 +68,7 @@ func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage cry ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareCertificateQuery(ctx, q.client) + query, scan := prepareCertificateQuery() if t.IsZero() { t = time.Now() } @@ -102,7 +101,7 @@ func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage cry return certs, nil } -func prepareCertificateQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Certificates, error)) { +func prepareCertificateQuery() (sq.SelectBuilder, func(*sql.Rows) (*Certificates, error)) { return sq.Select( KeyColID.identifier(), KeyColCreationDate.identifier(), @@ -117,7 +116,7 @@ func prepareCertificateQuery(ctx context.Context, db prepareDatabase) (sq.Select countColumn.identifier(), ).From(keyTable.identifier()). LeftJoin(join(CertificateColID, KeyColID)). - LeftJoin(join(KeyPrivateColID, KeyColID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(KeyPrivateColID, KeyColID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Certificates, error) { certificates := make([]Certificate, 0) diff --git a/internal/query/certificate_test.go b/internal/query/certificate_test.go index 01e563de11..eae011bb69 100644 --- a/internal/query/certificate_test.go +++ b/internal/query/certificate_test.go @@ -26,8 +26,7 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.keys4` + ` LEFT JOIN projections.keys4_certificate ON projections.keys4.id = projections.keys4_certificate.id AND projections.keys4.instance_id = projections.keys4_certificate.instance_id` + - ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` prepareCertificateCols = []string{ "id", "creation_date", @@ -142,7 +141,7 @@ func Test_CertificatePrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/current_state.go b/internal/query/current_state.go index 29497e6eec..6fae52713f 100644 --- a/internal/query/current_state.go +++ b/internal/query/current_state.go @@ -12,7 +12,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -68,7 +67,7 @@ func (q *Queries) SearchCurrentStates(ctx context.Context, queries *CurrentState ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareCurrentStateQuery(ctx, q.client) + query, scan := prepareCurrentStateQuery() stmt, args, err := queries.toQuery(query).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-MmFef", "Errors.Query.InvalidRequest") @@ -210,12 +209,12 @@ func reset(ctx context.Context, tx *sql.Tx, tables []string, projectionName stri return nil } -func prepareLatestState(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*State, error)) { +func prepareLatestState() (sq.SelectBuilder, func(*sql.Row) (*State, error)) { return sq.Select( CurrentStateColEventDate.identifier(), CurrentStateColPosition.identifier(), CurrentStateColLastUpdated.identifier()). - From(currentStateTable.identifier() + db.Timetravel(call.Took(ctx))). + From(currentStateTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*State, error) { var ( @@ -239,7 +238,7 @@ func prepareLatestState(ctx context.Context, db prepareDatabase) (sq.SelectBuild } } -func prepareCurrentStateQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*CurrentStates, error)) { +func prepareCurrentStateQuery() (sq.SelectBuilder, func(*sql.Rows) (*CurrentStates, error)) { return sq.Select( CurrentStateColLastUpdated.identifier(), CurrentStateColEventDate.identifier(), @@ -249,7 +248,7 @@ func prepareCurrentStateQuery(ctx context.Context, db prepareDatabase) (sq.Selec CurrentStateColAggregateID.identifier(), CurrentStateColSequence.identifier(), countColumn.identifier()). - From(currentStateTable.identifier() + db.Timetravel(call.Took(ctx))). + From(currentStateTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*CurrentStates, error) { states := make([]*CurrentState, 0) diff --git a/internal/query/current_state_test.go b/internal/query/current_state_test.go index c76dae710e..c0895dc439 100644 --- a/internal/query/current_state_test.go +++ b/internal/query/current_state_test.go @@ -19,8 +19,7 @@ var ( ` projections.current_states.aggregate_id,` + ` projections.current_states.sequence,` + ` COUNT(*) OVER ()` + - ` FROM projections.current_states` + - " AS OF SYSTEM TIME '-1 ms' " + ` FROM projections.current_states` currentSequenceCols = []string{ "last_updated", @@ -175,7 +174,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/custom_text.go b/internal/query/custom_text.go index e92c910b69..0bc909d614 100644 --- a/internal/query/custom_text.go +++ b/internal/query/custom_text.go @@ -13,7 +13,6 @@ import ( "sigs.k8s.io/yaml" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/i18n" @@ -90,7 +89,7 @@ func (q *Queries) CustomTextList(ctx context.Context, aggregateID, template, lan ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareCustomTextsQuery(ctx, q.client) + stmt, scan := prepareCustomTextsQuery() eq := sq.Eq{ CustomTextColAggregateID.identifier(): aggregateID, CustomTextColTemplate.identifier(): template, @@ -121,7 +120,7 @@ func (q *Queries) CustomTextListByTemplate(ctx context.Context, aggregateID, tem ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareCustomTextsQuery(ctx, q.client) + stmt, scan := prepareCustomTextsQuery() eq := sq.Eq{ CustomTextColAggregateID.identifier(): aggregateID, CustomTextColTemplate.identifier(): template, @@ -230,7 +229,7 @@ func (q *Queries) readLoginTranslationFile(ctx context.Context, lang string) ([] return contents, nil } -func prepareCustomTextsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*CustomTexts, error)) { +func prepareCustomTextsQuery() (sq.SelectBuilder, func(*sql.Rows) (*CustomTexts, error)) { return sq.Select( CustomTextColAggregateID.identifier(), CustomTextColSequence.identifier(), @@ -241,7 +240,7 @@ func prepareCustomTextsQuery(ctx context.Context, db prepareDatabase) (sq.Select CustomTextColKey.identifier(), CustomTextColText.identifier(), countColumn.identifier()). - From(customTextTable.identifier() + db.Timetravel(call.Took(ctx))). + From(customTextTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*CustomTexts, error) { customTexts := make([]*CustomText, 0) diff --git a/internal/query/custom_text_test.go b/internal/query/custom_text_test.go index 0453f71a2a..c31072793b 100644 --- a/internal/query/custom_text_test.go +++ b/internal/query/custom_text_test.go @@ -23,8 +23,7 @@ var ( ` projections.custom_texts2.key,` + ` projections.custom_texts2.text,` + ` COUNT(*) OVER ()` + - ` FROM projections.custom_texts2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.custom_texts2` prepareCustomTextsCols = []string{ "aggregate_id", "sequence", @@ -185,7 +184,7 @@ func Test_CustomTextPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/device_auth.go b/internal/query/device_auth.go index d63bfe0209..d2f86a44af 100644 --- a/internal/query/device_auth.go +++ b/internal/query/device_auth.go @@ -63,7 +63,7 @@ func (q *Queries) DeviceAuthRequestByUserCode(ctx context.Context, userCode stri ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareDeviceAuthQuery(ctx, q.client) + stmt, scan := prepareDeviceAuthQuery() eq := sq.Eq{ DeviceAuthRequestColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), DeviceAuthRequestColumnUserCode.identifier(): userCode, @@ -86,15 +86,24 @@ var deviceAuthSelectColumns = []string{ DeviceAuthRequestColumnUserCode.identifier(), DeviceAuthRequestColumnScopes.identifier(), DeviceAuthRequestColumnAudience.identifier(), + AppColumnName.identifier(), + ProjectColumnName.identifier(), } -func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.AuthRequestDevice, error)) { - return sq.Select(deviceAuthSelectColumns...).From(deviceAuthRequestTable.identifier()).PlaceholderFormat(sq.Dollar), +func prepareDeviceAuthQuery() (sq.SelectBuilder, func(*sql.Row) (*domain.AuthRequestDevice, error)) { + return sq.Select(deviceAuthSelectColumns...). + From(deviceAuthRequestTable.identifier()). + LeftJoin(join(AppOIDCConfigColumnClientID, DeviceAuthRequestColumnClientID)). + LeftJoin(join(AppColumnID, AppOIDCConfigColumnAppID)). + LeftJoin(join(ProjectColumnID, AppColumnProjectID)). + PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*domain.AuthRequestDevice, error) { dst := new(domain.AuthRequestDevice) var ( - scopes database.TextArray[string] - audience database.TextArray[string] + scopes database.TextArray[string] + audience database.TextArray[string] + appName sql.NullString + projectName sql.NullString ) err := row.Scan( @@ -103,15 +112,20 @@ func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectB &dst.UserCode, &scopes, &audience, + &appName, + &projectName, ) if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotExisting") + return nil, zerrors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotFound") } if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Voo3o", "Errors.Internal") } dst.Scopes = scopes dst.Audience = audience + dst.AppName = appName.String + dst.ProjectName = projectName.String + return dst, nil } } diff --git a/internal/query/device_auth_test.go b/internal/query/device_auth_test.go index f81a11b411..52ac50abb7 100644 --- a/internal/query/device_auth_test.go +++ b/internal/query/device_auth_test.go @@ -24,8 +24,17 @@ const ( ` projections.device_auth_requests2.device_code,` + ` projections.device_auth_requests2.user_code,` + ` projections.device_auth_requests2.scopes,` + - ` projections.device_auth_requests2.audience` + - ` FROM projections.device_auth_requests2` + ` projections.device_auth_requests2.audience,` + + ` projections.apps7.name,` + + ` projections.projects4.name` + + ` FROM projections.device_auth_requests2` + + ` LEFT JOIN projections.apps7_oidc_configs` + + ` ON projections.device_auth_requests2.client_id = projections.apps7_oidc_configs.client_id` + + ` AND projections.device_auth_requests2.instance_id = projections.apps7_oidc_configs.instance_id` + + ` LEFT JOIN projections.apps7 ON projections.apps7_oidc_configs.app_id = projections.apps7.id` + + ` AND projections.apps7_oidc_configs.instance_id = projections.apps7.instance_id` + + ` LEFT JOIN projections.projects4 ON projections.apps7.project_id = projections.projects4.id` + + ` AND projections.apps7.instance_id = projections.projects4.instance_id` expectedDeviceAuthWhereUserCodeQueryC = expectedDeviceAuthQueryC + ` WHERE projections.device_auth_requests2.instance_id = $1` + ` AND projections.device_auth_requests2.user_code = $2` @@ -40,13 +49,17 @@ var ( "user-code", database.TextArray[string]{"a", "b", "c"}, []string{"projectID", "clientID"}, + "appName", + "projectName", } expectedDeviceAuth = &domain.AuthRequestDevice{ - ClientID: "client-id", - DeviceCode: "device1", - UserCode: "user-code", - Scopes: []string{"a", "b", "c"}, - Audience: []string{"projectID", "clientID"}, + ClientID: "client-id", + DeviceCode: "device1", + UserCode: "user-code", + Scopes: []string{"a", "b", "c"}, + Audience: []string{"projectID", "clientID"}, + AppName: "appName", + ProjectName: "projectName", } ) @@ -125,7 +138,7 @@ func Test_prepareDeviceAuthQuery(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, prepareDeviceAuthQuery, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, prepareDeviceAuthQuery, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/domain_policy.go b/internal/query/domain_policy.go index d971723bcf..3eba664e75 100644 --- a/internal/query/domain_policy.go +++ b/internal/query/domain_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -118,7 +117,7 @@ func (q *Queries) DomainPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, } } - stmt, scan := prepareDomainPolicyQuery(ctx, q.client) + stmt, scan := prepareDomainPolicyQuery() query, args, err := stmt.Where(eq).OrderBy(DomainPolicyColIsDefault.identifier()). Limit(1).ToSql() if err != nil { @@ -136,7 +135,7 @@ func (q *Queries) DefaultDomainPolicy(ctx context.Context) (policy *DomainPolicy ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareDomainPolicyQuery(ctx, q.client) + stmt, scan := prepareDomainPolicyQuery() query, args, err := stmt.Where(sq.Eq{ DomainPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), DomainPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -154,7 +153,7 @@ func (q *Queries) DefaultDomainPolicy(ctx context.Context) (policy *DomainPolicy return policy, err } -func prepareDomainPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*DomainPolicy, error)) { +func prepareDomainPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*DomainPolicy, error)) { return sq.Select( DomainPolicyColID.identifier(), DomainPolicyColSequence.identifier(), @@ -167,7 +166,7 @@ func prepareDomainPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Selec DomainPolicyColIsDefault.identifier(), DomainPolicyColState.identifier(), ). - From(domainPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(domainPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*DomainPolicy, error) { policy := new(DomainPolicy) diff --git a/internal/query/domain_policy_test.go b/internal/query/domain_policy_test.go index 70d3ddc391..0ff2567979 100644 --- a/internal/query/domain_policy_test.go +++ b/internal/query/domain_policy_test.go @@ -23,8 +23,7 @@ var ( ` projections.domain_policies2.smtp_sender_address_matches_instance_domain,` + ` projections.domain_policies2.is_default,` + ` projections.domain_policies2.state` + - ` FROM projections.domain_policies2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.domain_policies2` prepareDomainPolicyCols = []string{ "id", "sequence", @@ -122,7 +121,7 @@ func Test_DomainPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/execution.go b/internal/query/execution.go index b98c680f57..0a2a989918 100644 --- a/internal/query/execution.go +++ b/internal/query/execution.go @@ -101,8 +101,8 @@ func (q *Queries) SearchExecutions(ctx context.Context, queries *ExecutionSearch eq := sq.Eq{ ExecutionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareExecutionsQuery(ctx, q.client) - return genericRowsQueryWithState[*Executions](ctx, q.client, executionTable, combineToWhereStmt(query, queries.toQuery, eq), scan) + query, scan := prepareExecutionsQuery() + return genericRowsQueryWithState(ctx, q.client, executionTable, combineToWhereStmt(query, queries.toQuery, eq), scan) } func (q *Queries) GetExecutionByID(ctx context.Context, id string) (execution *Execution, err error) { @@ -110,8 +110,8 @@ func (q *Queries) GetExecutionByID(ctx context.Context, id string) (execution *E ExecutionColumnID.identifier(): id, ExecutionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareExecutionQuery(ctx, q.client) - return genericRowQuery[*Execution](ctx, q.client, query.Where(eq), scan) + query, scan := prepareExecutionQuery() + return genericRowQuery(ctx, q.client, query.Where(eq), scan) } func NewExecutionInIDsSearchQuery(values []string) (SearchQuery, error) { @@ -219,7 +219,7 @@ func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string return execution, err } -func prepareExecutionQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { +func prepareExecutionQuery() (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { return sq.Select( ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), @@ -235,7 +235,7 @@ func prepareExecutionQuery(context.Context, prepareDatabase) (sq.SelectBuilder, scanExecution } -func prepareExecutionsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { +func prepareExecutionsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { return sq.Select( ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go index ee6bdc4d96..eaaac1e9ba 100644 --- a/internal/query/execution_test.go +++ b/internal/query/execution_test.go @@ -263,7 +263,7 @@ func Test_ExecutionPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/failed_events.go b/internal/query/failed_events.go index 7d2e875cee..c5ad1ae1d9 100644 --- a/internal/query/failed_events.go +++ b/internal/query/failed_events.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -83,7 +82,7 @@ type FailedEventSearchQueries struct { } func (q *Queries) SearchFailedEvents(ctx context.Context, queries *FailedEventSearchQueries) (failedEvents *FailedEvents, err error) { - query, scan := prepareFailedEventsQuery(ctx, q.client) + query, scan := prepareFailedEventsQuery() stmt, args, err := queries.toQuery(query).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-n8rjJ", "Errors.Query.InvalidRequest") @@ -139,7 +138,7 @@ func (q *FailedEventSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuil return query } -func prepareFailedEventsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*FailedEvents, error)) { +func prepareFailedEventsQuery() (sq.SelectBuilder, func(*sql.Rows) (*FailedEvents, error)) { return sq.Select( FailedEventsColumnProjectionName.identifier(), FailedEventsColumnFailedSequence.identifier(), @@ -149,7 +148,7 @@ func prepareFailedEventsQuery(ctx context.Context, db prepareDatabase) (sq.Selec FailedEventsColumnLastFailed.identifier(), FailedEventsColumnError.identifier(), countColumn.identifier()). - From(failedEventsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(failedEventsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*FailedEvents, error) { failedEvents := make([]*FailedEvent, 0) diff --git a/internal/query/failed_events_test.go b/internal/query/failed_events_test.go index 7e575b5891..25c15e9b8f 100644 --- a/internal/query/failed_events_test.go +++ b/internal/query/failed_events_test.go @@ -19,8 +19,7 @@ var ( ` projections.failed_events2.last_failed,` + ` projections.failed_events2.error,` + ` COUNT(*) OVER ()` + - ` FROM projections.failed_events2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.failed_events2` prepareFailedEventsCols = []string{ "projection_name", @@ -168,7 +167,7 @@ func Test_FailedEventsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/generic.go b/internal/query/generic.go index 70fc2884a2..ea2257c013 100644 --- a/internal/query/generic.go +++ b/internal/query/generic.go @@ -44,7 +44,7 @@ func genericRowsQueryWithState[R Stateful]( scan func(rows *sql.Rows) (R, error), ) (resp R, err error) { var rnil R - resp, err = genericRowsQuery[R](ctx, client, query, scan) + resp, err = genericRowsQuery(ctx, client, query, scan) if err != nil { return rnil, err } @@ -60,7 +60,7 @@ func latestState(ctx context.Context, client *database.DB, projections ...table) ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLatestState(ctx, client) + query, scan := prepareLatestState() or := make(sq.Or, len(projections)) for i, projection := range projections { or[i] = sq.Eq{CurrentStateColProjectionName.identifier(): projection.name} diff --git a/internal/query/iam_member.go b/internal/query/iam_member.go index 87b906aa51..139208c7b8 100644 --- a/internal/query/iam_member.go +++ b/internal/query/iam_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -71,7 +70,7 @@ func (q *Queries) IAMMembers(ctx context.Context, queries *IAMMembersQuery) (mem ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceMembersQuery(ctx, q.client) + query, scan := prepareInstanceMembersQuery() eq := sq.Eq{InstanceMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -94,7 +93,7 @@ func (q *Queries) IAMMembers(ctx context.Context, queries *IAMMembersQuery) (mem return members, err } -func prepareInstanceMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareInstanceMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( InstanceMemberCreationDate.identifier(), InstanceMemberChangeDate.identifier(), @@ -116,7 +115,7 @@ func prepareInstanceMembersQuery(ctx context.Context, db prepareDatabase) (sq.Se LeftJoin(join(HumanUserIDCol, InstanceMemberUserID)). LeftJoin(join(MachineUserIDCol, InstanceMemberUserID)). LeftJoin(join(UserIDCol, InstanceMemberUserID)). - LeftJoin(join(LoginNameUserIDCol, InstanceMemberUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, InstanceMemberUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/iam_member_test.go b/internal/query/iam_member_test.go index 82cea360c8..5c10ebc5bc 100644 --- a/internal/query/iam_member_test.go +++ b/internal/query/iam_member_test.go @@ -39,7 +39,6 @@ var ( "ON members.user_id = projections.users14.id AND members.instance_id = projections.users14.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id AND members.instance_id = projections.login_names3.instance_id " + - "AS OF SYSTEM TIME '-1 ms' " + "WHERE projections.login_names3.is_primary = $1") instanceMembersColumns = []string{ "creation_date", @@ -295,7 +294,7 @@ func Test_IAMMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/idp.go b/internal/query/idp.go index 2687397330..4f0d77c62b 100644 --- a/internal/query/idp.go +++ b/internal/query/idp.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" @@ -215,7 +214,7 @@ func (q *Queries) IDPByIDAndResourceOwner(ctx context.Context, shouldTriggerBulk sq.Eq{IDPResourceOwnerCol.identifier(): authz.GetInstance(ctx).InstanceID()}, }, } - stmt, scan := prepareIDPByIDQuery(ctx, q.client) + stmt, scan := prepareIDPByIDQuery() query, args, err := stmt.Where(where).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-0gocI", "Errors.Query.SQLStatement") @@ -233,7 +232,7 @@ func (q *Queries) IDPs(ctx context.Context, queries *IDPSearchQueries, withOwner ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPsQuery(ctx, q.client) + query, scan := prepareIDPsQuery() eq := sq.Eq{ IDPInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -293,7 +292,7 @@ func (q *IDPSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query } -func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*IDP, error)) { +func prepareIDPByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDP, error)) { return sq.Select( IDPIDCol.identifier(), IDPResourceOwnerCol.identifier(), @@ -321,7 +320,7 @@ func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil JWTIDPColEndpoint.identifier(), ).From(idpTable.identifier()). LeftJoin(join(OIDCIDPColIDPID, IDPIDCol)). - LeftJoin(join(JWTIDPColIDPID, IDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(JWTIDPColIDPID, IDPIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*IDP, error) { idp := new(IDP) @@ -401,7 +400,7 @@ func prepareIDPByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareIDPsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPs, error)) { +func prepareIDPsQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPs, error)) { return sq.Select( IDPIDCol.identifier(), IDPResourceOwnerCol.identifier(), @@ -430,7 +429,7 @@ func prepareIDPsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder countColumn.identifier(), ).From(idpTable.identifier()). LeftJoin(join(OIDCIDPColIDPID, IDPIDCol)). - LeftJoin(join(JWTIDPColIDPID, IDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(JWTIDPColIDPID, IDPIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPs, error) { idps := make([]*IDP, 0) diff --git a/internal/query/idp_login_policy_link.go b/internal/query/idp_login_policy_link.go index bdc2ef15b1..65f855bc51 100644 --- a/internal/query/idp_login_policy_link.go +++ b/internal/query/idp_login_policy_link.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -97,7 +96,7 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPLoginPolicyLinksQuery(ctx, q.client, resourceOwner) + query, scan := prepareIDPLoginPolicyLinksQuery(ctx, resourceOwner) eq := sq.Eq{ IDPLoginPolicyLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -122,7 +121,8 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, return idps, err } -func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase, resourceOwner string) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { +//nolint:gocognit +func prepareIDPLoginPolicyLinksQuery(ctx context.Context, resourceOwner string) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { resourceOwnerQuery, resourceOwnerArgs, err := prepareIDPLoginPolicyLinksResourceOwnerQuery(ctx, resourceOwner) if err != nil { return sq.SelectBuilder{}, nil @@ -142,8 +142,7 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase, re LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol)). RightJoin("("+resourceOwnerQuery+") AS "+idpLoginPolicyOwnerTable.alias+" ON "+ idpLoginPolicyOwnerIDCol.identifier()+" = "+IDPLoginPolicyLinkResourceOwnerCol.identifier()+" AND "+ - idpLoginPolicyOwnerInstanceIDCol.identifier()+" = "+IDPLoginPolicyLinkInstanceIDCol.identifier()+ - " "+db.Timetravel(call.Took(ctx)), + idpLoginPolicyOwnerInstanceIDCol.identifier()+" = "+IDPLoginPolicyLinkInstanceIDCol.identifier(), resourceOwnerArgs...). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPLoginPolicyLinks, error) { diff --git a/internal/query/idp_login_policy_link_test.go b/internal/query/idp_login_policy_link_test.go index 245eb22ccc..9f66e118ea 100644 --- a/internal/query/idp_login_policy_link_test.go +++ b/internal/query/idp_login_policy_link_test.go @@ -6,6 +6,7 @@ import ( "database/sql/driver" "errors" "fmt" + "reflect" "regexp" "testing" @@ -29,8 +30,7 @@ var ( ` LEFT JOIN projections.idp_templates6 ON projections.idp_login_policy_links5.idp_id = projections.idp_templates6.id AND projections.idp_login_policy_links5.instance_id = projections.idp_templates6.instance_id` + ` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies5 AS login_policy_owner` + ` WHERE (login_policy_owner.instance_id = $1 AND (login_policy_owner.aggregate_id = $2 OR login_policy_owner.aggregate_id = $3)) ORDER BY login_policy_owner.is_default LIMIT 1) AS login_policy_owner` + - ` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id`) loginPolicyIDPLinksCols = []string{ "idp_id", "name", @@ -52,14 +52,14 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { } tests := []struct { name string - prepare interface{} + prepare any want want - object interface{} + object any }{ { name: "prepareIDPsQuery found", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueries( @@ -101,8 +101,8 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, { name: "prepareIDPsQuery no idp", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueries( @@ -143,8 +143,8 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, { name: "prepareIDPsQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { - return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner") + prepare: func(ctx context.Context) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { + return prepareIDPLoginPolicyLinksQuery(ctx, "resourceOwner") }, want: want{ sqlExpectations: mockQueryErr( @@ -163,7 +163,7 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, reflect.ValueOf(context.Background())) }) } } diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index dd36cee196..f51e9a11a7 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" @@ -64,6 +63,7 @@ type OAuthIDPTemplate struct { UserEndpoint string Scopes database.TextArray[string] IDAttribute string + UsePKCE bool } type OIDCIDPTemplate struct { @@ -73,6 +73,7 @@ type OIDCIDPTemplate struct { Issuer string Scopes database.TextArray[string] IsIDTokenMapping bool + UsePKCE bool } type JWTIDPTemplate struct { @@ -142,6 +143,7 @@ type LDAPIDPTemplate struct { UserObjectClasses []string UserFilters []string Timeout time.Duration + RootCA []byte idp.LDAPAttributes } @@ -277,6 +279,10 @@ var ( name: projection.OAuthIDAttributeCol, table: oauthIdpTemplateTable, } + OAuthUsePKCECol = Column{ + name: projection.OAuthUsePKCECol, + table: oauthIdpTemplateTable, + } ) var ( @@ -312,6 +318,10 @@ var ( name: projection.OIDCIDTokenMappingCol, table: oidcIdpTemplateTable, } + OIDCUsePKCECol = Column{ + name: projection.OIDCUsePKCECol, + table: oidcIdpTemplateTable, + } ) var ( @@ -580,6 +590,10 @@ var ( name: projection.LDAPTimeoutCol, table: ldapIdpTemplateTable, } + LDAPRootCACol = Column{ + name: projection.LDAPRootCACol, + table: ldapIdpTemplateTable, + } LDAPIDAttributeCol = Column{ name: projection.LDAPIDAttributeCol, table: ldapIdpTemplateTable, @@ -752,7 +766,7 @@ func (q *Queries) idpTemplateByID(ctx context.Context, shouldTriggerBulk bool, i if !withOwnerRemoved { eq[IDPTemplateOwnerRemovedCol.identifier()] = false } - query, scan := prepareIDPTemplateByIDQuery(ctx, q.client) + query, scan := prepareIDPTemplateByIDQuery() for _, q := range queries { query = q.toQuery(query) } @@ -773,7 +787,7 @@ func (q *Queries) IDPTemplates(ctx context.Context, queries *IDPTemplateSearchQu ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPTemplatesQuery(ctx, q.client) + query, scan := prepareIDPTemplatesQuery() eq := sq.Eq{ IDPTemplateInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -849,7 +863,7 @@ func (q *IDPTemplateSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuil return query } -func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*IDPTemplate, error)) { +func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTemplate, error)) { return sq.Select( IDPTemplateIDCol.identifier(), IDPTemplateResourceOwnerCol.identifier(), @@ -874,6 +888,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se OAuthUserEndpointCol.identifier(), OAuthScopesCol.identifier(), OAuthIDAttributeCol.identifier(), + OAuthUsePKCECol.identifier(), // oidc OIDCIDCol.identifier(), OIDCIssuerCol.identifier(), @@ -881,6 +896,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se OIDCClientSecretCol.identifier(), OIDCScopesCol.identifier(), OIDCIDTokenMappingCol.identifier(), + OIDCUsePKCECol.identifier(), // jwt JWTIDCol.identifier(), JWTIssuerCol.identifier(), @@ -943,6 +959,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se LDAPUserObjectClassesCol.identifier(), LDAPUserFiltersCol.identifier(), LDAPTimeoutCol.identifier(), + LDAPRootCACol.identifier(), LDAPIDAttributeCol.identifier(), LDAPFirstNameAttributeCol.identifier(), LDAPLastNameAttributeCol.identifier(), @@ -975,7 +992,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(SAMLIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). - LeftJoin(join(AppleIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppleIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*IDPTemplate, error) { idpTemplate := new(IDPTemplate) @@ -990,6 +1007,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se oauthUserEndpoint := sql.NullString{} oauthScopes := database.TextArray[string]{} oauthIDAttribute := sql.NullString{} + oauthUserPKCE := sql.NullBool{} oidcID := sql.NullString{} oidcIssuer := sql.NullString{} @@ -997,6 +1015,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se oidcClientSecret := new(crypto.CryptoValue) oidcScopes := database.TextArray[string]{} oidcIDTokenMapping := sql.NullBool{} + oidcUserPKCE := sql.NullBool{} jwtID := sql.NullString{} jwtIssuer := sql.NullString{} @@ -1059,6 +1078,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se ldapUserObjectClasses := database.TextArray[string]{} ldapUserFilters := database.TextArray[string]{} ldapTimeout := sql.NullInt64{} + var ldapRootCA []byte ldapIDAttribute := sql.NullString{} ldapFirstNameAttribute := sql.NullString{} ldapLastNameAttribute := sql.NullString{} @@ -1104,6 +1124,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se &oauthUserEndpoint, &oauthScopes, &oauthIDAttribute, + &oauthUserPKCE, // oidc &oidcID, &oidcIssuer, @@ -1111,6 +1132,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se &oidcClientSecret, &oidcScopes, &oidcIDTokenMapping, + &oidcUserPKCE, // jwt &jwtID, &jwtIssuer, @@ -1173,6 +1195,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se &ldapUserObjectClasses, &ldapUserFilters, &ldapTimeout, + &ldapRootCA, &ldapIDAttribute, &ldapFirstNameAttribute, &ldapLastNameAttribute, @@ -1213,6 +1236,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se UserEndpoint: oauthUserEndpoint.String, Scopes: oauthScopes, IDAttribute: oauthIDAttribute.String, + UsePKCE: oauthUserPKCE.Bool, } } if oidcID.Valid { @@ -1223,6 +1247,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se Issuer: oidcIssuer.String, Scopes: oidcScopes, IsIDTokenMapping: oidcIDTokenMapping.Bool, + UsePKCE: oidcUserPKCE.Bool, } } if jwtID.Valid { @@ -1312,6 +1337,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se UserObjectClasses: ldapUserObjectClasses, UserFilters: ldapUserFilters, Timeout: time.Duration(ldapTimeout.Int64), + RootCA: ldapRootCA, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: ldapIDAttribute.String, FirstNameAttribute: ldapFirstNameAttribute.String, @@ -1344,7 +1370,8 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplates, error)) { +//nolint:gocognit +func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplates, error)) { return sq.Select( IDPTemplateIDCol.identifier(), IDPTemplateResourceOwnerCol.identifier(), @@ -1369,6 +1396,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec OAuthUserEndpointCol.identifier(), OAuthScopesCol.identifier(), OAuthIDAttributeCol.identifier(), + OAuthUsePKCECol.identifier(), // oidc OIDCIDCol.identifier(), OIDCIssuerCol.identifier(), @@ -1376,6 +1404,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec OIDCClientSecretCol.identifier(), OIDCScopesCol.identifier(), OIDCIDTokenMappingCol.identifier(), + OIDCUsePKCECol.identifier(), // jwt JWTIDCol.identifier(), JWTIssuerCol.identifier(), @@ -1438,6 +1467,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec LDAPUserObjectClassesCol.identifier(), LDAPUserFiltersCol.identifier(), LDAPTimeoutCol.identifier(), + LDAPRootCACol.identifier(), LDAPIDAttributeCol.identifier(), LDAPFirstNameAttributeCol.identifier(), LDAPLastNameAttributeCol.identifier(), @@ -1472,7 +1502,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(SAMLIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). - LeftJoin(join(AppleIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(AppleIDCol, IDPTemplateIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPTemplates, error) { templates := make([]*IDPTemplate, 0) @@ -1490,6 +1520,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec oauthUserEndpoint := sql.NullString{} oauthScopes := database.TextArray[string]{} oauthIDAttribute := sql.NullString{} + oauthUserPKCE := sql.NullBool{} oidcID := sql.NullString{} oidcIssuer := sql.NullString{} @@ -1497,6 +1528,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec oidcClientSecret := new(crypto.CryptoValue) oidcScopes := database.TextArray[string]{} oidcIDTokenMapping := sql.NullBool{} + oidcUserPKCE := sql.NullBool{} jwtID := sql.NullString{} jwtIssuer := sql.NullString{} @@ -1559,6 +1591,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec ldapUserObjectClasses := database.TextArray[string]{} ldapUserFilters := database.TextArray[string]{} ldapTimeout := sql.NullInt64{} + var ldapRootCA []byte ldapIDAttribute := sql.NullString{} ldapFirstNameAttribute := sql.NullString{} ldapLastNameAttribute := sql.NullString{} @@ -1604,6 +1637,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec &oauthUserEndpoint, &oauthScopes, &oauthIDAttribute, + &oauthUserPKCE, // oidc &oidcID, &oidcIssuer, @@ -1611,6 +1645,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec &oidcClientSecret, &oidcScopes, &oidcIDTokenMapping, + &oidcUserPKCE, // jwt &jwtID, &jwtIssuer, @@ -1673,6 +1708,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec &ldapUserObjectClasses, &ldapUserFilters, &ldapTimeout, + &ldapRootCA, &ldapIDAttribute, &ldapFirstNameAttribute, &ldapLastNameAttribute, @@ -1712,6 +1748,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec UserEndpoint: oauthUserEndpoint.String, Scopes: oauthScopes, IDAttribute: oauthIDAttribute.String, + UsePKCE: oauthUserPKCE.Bool, } } if oidcID.Valid { @@ -1722,6 +1759,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec Issuer: oidcIssuer.String, Scopes: oidcScopes, IsIDTokenMapping: oidcIDTokenMapping.Bool, + UsePKCE: oidcUserPKCE.Bool, } } if jwtID.Valid { @@ -1811,6 +1849,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec UserObjectClasses: ldapUserObjectClasses, UserFilters: ldapUserFilters, Timeout: time.Duration(ldapTimeout.Int64), + RootCA: ldapRootCA, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: ldapIDAttribute.String, FirstNameAttribute: ldapFirstNameAttribute.String, diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index c5bee3a000..702e5d0ced 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -39,6 +39,7 @@ var ( ` projections.idp_templates6_oauth2.user_endpoint,` + ` projections.idp_templates6_oauth2.scopes,` + ` projections.idp_templates6_oauth2.id_attribute,` + + ` projections.idp_templates6_oauth2.use_pkce,` + // oidc ` projections.idp_templates6_oidc.idp_id,` + ` projections.idp_templates6_oidc.issuer,` + @@ -46,6 +47,7 @@ var ( ` projections.idp_templates6_oidc.client_secret,` + ` projections.idp_templates6_oidc.scopes,` + ` projections.idp_templates6_oidc.id_token_mapping,` + + ` projections.idp_templates6_oidc.use_pkce,` + // jwt ` projections.idp_templates6_jwt.idp_id,` + ` projections.idp_templates6_jwt.issuer,` + @@ -108,6 +110,7 @@ var ( ` projections.idp_templates6_ldap2.user_object_classes,` + ` projections.idp_templates6_ldap2.user_filters,` + ` projections.idp_templates6_ldap2.timeout,` + + ` projections.idp_templates6_ldap2.root_ca,` + ` projections.idp_templates6_ldap2.id_attribute,` + ` projections.idp_templates6_ldap2.first_name_attribute,` + ` projections.idp_templates6_ldap2.last_name_attribute,` + @@ -140,8 +143,7 @@ var ( ` LEFT JOIN projections.idp_templates6_google ON projections.idp_templates6.id = projections.idp_templates6_google.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_google.instance_id` + ` LEFT JOIN projections.idp_templates6_saml ON projections.idp_templates6.id = projections.idp_templates6_saml.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_saml.instance_id` + ` LEFT JOIN projections.idp_templates6_ldap2 ON projections.idp_templates6.id = projections.idp_templates6_ldap2.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_ldap2.instance_id` + - ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` idpTemplateCols = []string{ "id", "resource_owner", @@ -166,6 +168,7 @@ var ( "user_endpoint", "scopes", "id_attribute", + "use_pkce", // oidc config "id_id", "issuer", @@ -173,6 +176,7 @@ var ( "client_secret", "scopes", "id_token_mapping", + "use_pkce", // jwt "idp_id", "issuer", @@ -235,6 +239,7 @@ var ( "user_object_classes", "user_filters", "timeout", + "root_ca", "id_attribute", "first_name_attribute", "last_name_attribute", @@ -279,6 +284,7 @@ var ( ` projections.idp_templates6_oauth2.user_endpoint,` + ` projections.idp_templates6_oauth2.scopes,` + ` projections.idp_templates6_oauth2.id_attribute,` + + ` projections.idp_templates6_oauth2.use_pkce,` + // oidc ` projections.idp_templates6_oidc.idp_id,` + ` projections.idp_templates6_oidc.issuer,` + @@ -286,6 +292,7 @@ var ( ` projections.idp_templates6_oidc.client_secret,` + ` projections.idp_templates6_oidc.scopes,` + ` projections.idp_templates6_oidc.id_token_mapping,` + + ` projections.idp_templates6_oidc.use_pkce,` + // jwt ` projections.idp_templates6_jwt.idp_id,` + ` projections.idp_templates6_jwt.issuer,` + @@ -348,6 +355,7 @@ var ( ` projections.idp_templates6_ldap2.user_object_classes,` + ` projections.idp_templates6_ldap2.user_filters,` + ` projections.idp_templates6_ldap2.timeout,` + + ` projections.idp_templates6_ldap2.root_ca,` + ` projections.idp_templates6_ldap2.id_attribute,` + ` projections.idp_templates6_ldap2.first_name_attribute,` + ` projections.idp_templates6_ldap2.last_name_attribute,` + @@ -381,8 +389,7 @@ var ( ` LEFT JOIN projections.idp_templates6_google ON projections.idp_templates6.id = projections.idp_templates6_google.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_google.instance_id` + ` LEFT JOIN projections.idp_templates6_saml ON projections.idp_templates6.id = projections.idp_templates6_saml.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_saml.instance_id` + ` LEFT JOIN projections.idp_templates6_ldap2 ON projections.idp_templates6.id = projections.idp_templates6_ldap2.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_ldap2.instance_id` + - ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.idp_templates6_apple ON projections.idp_templates6.id = projections.idp_templates6_apple.idp_id AND projections.idp_templates6.instance_id = projections.idp_templates6_apple.instance_id` idpTemplatesCols = []string{ "id", "resource_owner", @@ -407,6 +414,7 @@ var ( "user_endpoint", "scopes", "id_attribute", + "use_pkce", // oidc config "id_id", "issuer", @@ -414,6 +422,7 @@ var ( "client_secret", "scopes", "id_token_mapping", + "use_pkce", // jwt "idp_id", "issuer", @@ -476,6 +485,7 @@ var ( "user_object_classes", "user_filters", "timeout", + "root_ca", "id_attribute", "first_name_attribute", "last_name_attribute", @@ -560,6 +570,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { "user", database.TextArray[string]{"profile"}, "id-attribute", + true, // oidc nil, nil, @@ -567,6 +578,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -642,6 +654,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -676,6 +689,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { UserEndpoint: "user", Scopes: []string{"profile"}, IDAttribute: "id-attribute", + UsePKCE: true, }, }, }, @@ -710,6 +724,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc "idp-id", "issuer", @@ -717,6 +732,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, database.TextArray[string]{"profile"}, true, + true, // jwt nil, nil, @@ -792,6 +808,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -824,6 +841,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { ClientSecret: nil, Scopes: []string{"profile"}, IsIDTokenMapping: true, + UsePKCE: true, }, }, }, @@ -858,6 +876,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -865,6 +884,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt "idp-id", "issuer", @@ -940,6 +960,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -1005,6 +1026,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -1012,6 +1034,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -1087,6 +1110,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -1151,6 +1175,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -1158,6 +1183,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -1233,6 +1259,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -1297,6 +1324,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -1304,6 +1332,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -1379,6 +1408,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -1444,6 +1474,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -1451,6 +1482,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -1526,6 +1558,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -1590,6 +1623,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -1597,6 +1631,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -1672,6 +1707,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -1740,6 +1776,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -1747,6 +1784,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -1809,6 +1847,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { database.TextArray[string]{"object"}, database.TextArray[string]{"filter"}, time.Duration(30000000000), + []byte("certificate"), "id", "first", "last", @@ -1857,6 +1896,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { UserObjectClasses: []string{"object"}, UserFilters: []string{"filter"}, Timeout: time.Duration(30000000000), + RootCA: []byte("certificate"), LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "first", @@ -1906,6 +1946,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -1913,6 +1954,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -1988,6 +2030,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple "idp-id", "client_id", @@ -2054,6 +2097,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -2061,6 +2105,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -2136,6 +2181,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -2230,6 +2276,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -2237,6 +2284,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -2299,6 +2347,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { database.TextArray[string]{"object"}, database.TextArray[string]{"filter"}, time.Duration(30000000000), + []byte("certificate"), "id", "first", "last", @@ -2353,6 +2402,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { UserObjectClasses: []string{"object"}, UserFilters: []string{"filter"}, Timeout: time.Duration(30000000000), + RootCA: []byte("certificate"), LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "first", @@ -2405,6 +2455,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -2412,6 +2463,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -2487,6 +2539,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -2554,6 +2607,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -2561,6 +2615,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -2623,6 +2678,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { database.TextArray[string]{"object"}, database.TextArray[string]{"filter"}, time.Duration(30000000000), + []byte("certificate"), "id", "first", "last", @@ -2668,6 +2724,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -2675,6 +2732,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -2750,6 +2808,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -2782,6 +2841,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -2789,6 +2849,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -2864,6 +2925,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -2896,6 +2958,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { "user", database.TextArray[string]{"profile"}, "id-attribute", + true, // oidc nil, nil, @@ -2903,6 +2966,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt nil, nil, @@ -2978,6 +3042,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -3010,6 +3075,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc "idp-id-oidc", "issuer", @@ -3017,6 +3083,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, database.TextArray[string]{"profile"}, true, + true, // jwt nil, nil, @@ -3092,6 +3159,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -3124,6 +3192,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // oidc nil, nil, @@ -3131,6 +3200,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // jwt "idp-id-jwt", "issuer", @@ -3206,6 +3276,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // apple nil, nil, @@ -3247,6 +3318,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { UserObjectClasses: []string{"object"}, UserFilters: []string{"filter"}, Timeout: time.Duration(30000000000), + RootCA: []byte("certificate"), LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "first", @@ -3337,6 +3409,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { UserEndpoint: "user", Scopes: []string{"profile"}, IDAttribute: "id-attribute", + UsePKCE: true, }, }, { @@ -3361,6 +3434,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { ClientSecret: nil, Scopes: []string{"profile"}, IsIDTokenMapping: true, + UsePKCE: true, }, }, { @@ -3409,7 +3483,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/idp_test.go b/internal/query/idp_test.go index 9474a0c751..a7f6fb95c1 100644 --- a/internal/query/idp_test.go +++ b/internal/query/idp_test.go @@ -733,7 +733,7 @@ func Test_IDPPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/idp_user_link.go b/internal/query/idp_user_link.go index 5caf6c6646..7f162f235e 100644 --- a/internal/query/idp_user_link.go +++ b/internal/query/idp_user_link.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -107,12 +106,27 @@ func idpLinksCheckPermission(ctx context.Context, links *IDPUserLinks, permissio ) } +func idpLinksPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *IDPUserLinksSearchQuery) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + IDPUserLinkResourceOwnerCol, + domain.PermissionUserRead, + SingleOrgPermissionOption(queries.Queries), + OwnedRowsPermissionOption(IDPUserLinkUserIDCol), + ) + return query.JoinClause(join, args...) +} + func (q *Queries) IDPUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, permissionCheck domain.PermissionCheck) (idps *IDPUserLinks, err error) { - links, err := q.idpUserLinks(ctx, queries, false) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + links, err := q.idpUserLinks(ctx, queries, permissionCheckV2) if err != nil { return nil, err } - if permissionCheck != nil && len(links.Links) > 0 { + if permissionCheck != nil && len(links.Links) > 0 && !permissionCheckV2 { // when userID for query is provided, only one check has to be done if queries.hasUserID() { if err := userCheckPermission(ctx, links.Links[0].ResourceOwner, links.Links[0].UserID, permissionCheck); err != nil { @@ -125,14 +139,15 @@ func (q *Queries) IDPUserLinks(ctx context.Context, queries *IDPUserLinksSearchQ return links, nil } -func (q *Queries) idpUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, withOwnerRemoved bool) (idps *IDPUserLinks, err error) { +func (q *Queries) idpUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, permissionCheckV2 bool) (idps *IDPUserLinks, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareIDPUserLinksQuery(ctx, q.client) - eq := sq.Eq{IDPUserLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} - if !withOwnerRemoved { - eq[IDPUserLinkOwnerRemovedCol.identifier()] = false + query, scan := prepareIDPUserLinksQuery() + query = idpLinksPermissionCheckV2(ctx, query, permissionCheckV2, queries) + eq := sq.Eq{ + IDPUserLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + IDPUserLinkOwnerRemovedCol.identifier(): false, } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -166,7 +181,7 @@ func NewIDPUserLinksExternalIDSearchQuery(value string) (SearchQuery, error) { return NewTextQuery(IDPUserLinkExternalUserIDCol, value, TextEquals) } -func prepareIDPUserLinksQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPUserLinks, error)) { +func prepareIDPUserLinksQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPUserLinks, error)) { return sq.Select( IDPUserLinkIDPIDCol.identifier(), IDPUserLinkUserIDCol.identifier(), @@ -177,7 +192,7 @@ func prepareIDPUserLinksQuery(ctx context.Context, db prepareDatabase) (sq.Selec IDPUserLinkResourceOwnerCol.identifier(), countColumn.identifier()). From(idpUserLinkTable.identifier()). - LeftJoin(join(IDPTemplateIDCol, IDPUserLinkIDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(IDPTemplateIDCol, IDPUserLinkIDPIDCol)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPUserLinks, error) { idps := make([]*IDPUserLink, 0) diff --git a/internal/query/idp_user_link_test.go b/internal/query/idp_user_link_test.go index b8ba2d087a..eac9669110 100644 --- a/internal/query/idp_user_link_test.go +++ b/internal/query/idp_user_link_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "regexp" + "slices" "testing" "github.com/stretchr/testify/require" @@ -165,10 +166,8 @@ func TestUser_idpLinksCheckPermission(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - for _, perm := range tt.permissions { - if resourceID == perm { - return nil - } + if slices.Contains(tt.permissions, resourceID) { + return nil } return errors.New("failed") } @@ -188,8 +187,7 @@ var ( ` projections.idp_user_links3.resource_owner,` + ` COUNT(*) OVER ()` + ` FROM projections.idp_user_links3` + - ` LEFT JOIN projections.idp_templates6 ON projections.idp_user_links3.idp_id = projections.idp_templates6.id AND projections.idp_user_links3.instance_id = projections.idp_templates6.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.idp_templates6 ON projections.idp_user_links3.idp_id = projections.idp_templates6.id AND projections.idp_user_links3.instance_id = projections.idp_templates6.instance_id`) idpUserLinksCols = []string{ "idp_id", "user_id", @@ -307,7 +305,7 @@ func Test_IDPUserLinkPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/instance.go b/internal/query/instance.go index d7d66b1607..1b3bb055cb 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -16,7 +16,6 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -151,7 +150,7 @@ func (q *Queries) SearchInstances(ctx context.Context, queries *InstanceSearchQu ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - filter, query, scan := prepareInstancesQuery(ctx, q.client) + filter, query, scan := prepareInstancesQuery() stmt, args, err := query(queries.toQuery(filter)).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-M9fow", "Errors.Query.SQLStatement") @@ -178,7 +177,7 @@ func (q *Queries) Instance(ctx context.Context, shouldTriggerBulk bool) (instanc traceSpan.EndWithError(err) } - stmt, scan := prepareInstanceDomainQuery(ctx, q.client) + stmt, scan := prepareInstanceDomainQuery() query, args, err := stmt.Where(sq.Eq{ InstanceColumnID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() @@ -261,7 +260,7 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag { return instance.DefaultLang } -func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { +func prepareInstancesQuery() (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { instanceFilterTable := instanceTable.setAlias(InstancesFilterTableAlias) instanceFilterIDColumn := InstanceColumnID.setTable(instanceFilterTable) instanceFilterCountColumn := InstancesFilterTableAlias + ".count" @@ -291,7 +290,7 @@ func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu InstanceDomainSequenceCol.identifier(), ).FromSelect(builder, InstancesFilterTableAlias). LeftJoin(join(InstanceColumnID, instanceFilterIDColumn)). - LeftJoin(join(InstanceDomainInstanceIDCol, instanceFilterIDColumn) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(InstanceDomainInstanceIDCol, instanceFilterIDColumn)). PlaceholderFormat(sq.Dollar) }, func(rows *sql.Rows) (*Instances, error) { @@ -366,7 +365,7 @@ func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) { +func prepareInstanceDomainQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) { return sq.Select( InstanceColumnID.identifier(), InstanceColumnCreationDate.identifier(), @@ -386,7 +385,7 @@ func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase) (sq.Sel InstanceDomainSequenceCol.identifier(), ). From(instanceTable.identifier()). - LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Instance, error) { instance := &Instance{ diff --git a/internal/query/instance_domain.go b/internal/query/instance_domain.go index 285bd12936..47b5fab27f 100644 --- a/internal/query/instance_domain.go +++ b/internal/query/instance_domain.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -62,7 +61,7 @@ func (q *Queries) SearchInstanceDomains(ctx context.Context, queries *InstanceDo ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceDomainsQuery(ctx, q.client) + query, scan := prepareInstanceDomainsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ InstanceDomainInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -78,7 +77,7 @@ func (q *Queries) SearchInstanceDomainsGlobal(ctx context.Context, queries *Inst ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceDomainsQuery(ctx, q.client) + query, scan := prepareInstanceDomainsQuery() stmt, args, err := queries.toQuery(query).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-IHhLR", "Errors.Query.SQLStatement") @@ -99,7 +98,7 @@ func (q *Queries) queryInstanceDomains(ctx context.Context, stmt string, scan fu return domains, err } -func prepareInstanceDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*InstanceDomains, error)) { +func prepareInstanceDomainsQuery() (sq.SelectBuilder, func(*sql.Rows) (*InstanceDomains, error)) { return sq.Select( InstanceDomainCreationDateCol.identifier(), InstanceDomainChangeDateCol.identifier(), @@ -109,7 +108,7 @@ func prepareInstanceDomainsQuery(ctx context.Context, db prepareDatabase) (sq.Se InstanceDomainIsGeneratedCol.identifier(), InstanceDomainIsPrimaryCol.identifier(), countColumn.identifier(), - ).From(instanceDomainsTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(instanceDomainsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*InstanceDomains, error) { domains := make([]*InstanceDomain, 0) diff --git a/internal/query/instance_domain_test.go b/internal/query/instance_domain_test.go index 4f72c0def4..fd147bf4b7 100644 --- a/internal/query/instance_domain_test.go +++ b/internal/query/instance_domain_test.go @@ -18,8 +18,7 @@ var ( ` projections.instance_domains.is_generated,` + ` projections.instance_domains.is_primary,` + ` COUNT(*) OVER ()` + - ` FROM projections.instance_domains` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.instance_domains` prepareInstanceDomainsCols = []string{ "creation_date", "change_date", @@ -167,7 +166,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go index 646404ce6c..4ec40dc9d5 100644 --- a/internal/query/instance_features.go +++ b/internal/query/instance_features.go @@ -14,7 +14,6 @@ type InstanceFeatures struct { LegacyIntrospection FeatureSource[bool] UserSchema FeatureSource[bool] TokenExchange FeatureSource[bool] - Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] WebKey FeatureSource[bool] DebugOIDCParentError FeatureSource[bool] @@ -23,6 +22,7 @@ type InstanceFeatures struct { EnableBackChannelLogout FeatureSource[bool] LoginV2 FeatureSource[*feature.LoginV2] PermissionCheckV2 FeatureSource[bool] + ConsoleUseV2UserApi FeatureSource[bool] } func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) { diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go index b9839bf359..6a0abbb58c 100644 --- a/internal/query/instance_features_model.go +++ b/internal/query/instance_features_model.go @@ -67,7 +67,6 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceLegacyIntrospectionEventType, feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, - feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, @@ -76,6 +75,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceEnableBackChannelLogout, feature_v2.InstanceLoginVersion, feature_v2.InstancePermissionCheckV2, + feature_v2.InstanceConsoleUseV2UserApi, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -97,7 +97,6 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool { m.instance.LegacyIntrospection = m.system.LegacyIntrospection m.instance.UserSchema = m.system.UserSchema m.instance.TokenExchange = m.system.TokenExchange - m.instance.Actions = m.system.Actions m.instance.ImprovedPerformance = m.system.ImprovedPerformance m.instance.OIDCSingleV1SessionTermination = m.system.OIDCSingleV1SessionTermination m.instance.DisableUserTokenEvent = m.system.DisableUserTokenEvent @@ -112,7 +111,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ return err } switch key { - case feature.KeyUnspecified: + case feature.KeyUnspecified, + feature.KeyActionsDeprecated: return nil case feature.KeyLoginDefaultOrg: features.LoginDefaultOrg.set(level, event.Value) @@ -124,8 +124,6 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ features.UserSchema.set(level, event.Value) case feature.KeyTokenExchange: features.TokenExchange.set(level, event.Value) - case feature.KeyActions: - features.Actions.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) case feature.KeyWebKey: @@ -142,6 +140,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ features.LoginV2.set(level, event.Value) case feature.KeyPermissionCheckV2: features.PermissionCheckV2.set(level, event.Value) + case feature.KeyConsoleUseV2UserApi: + features.ConsoleUseV2UserApi.set(level, event.Value) } return nil } diff --git a/internal/query/instance_features_test.go b/internal/query/instance_features_test.go index e182f4002f..d80a3b05fc 100644 --- a/internal/query/instance_features_test.go +++ b/internal/query/instance_features_test.go @@ -84,31 +84,27 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { { name: "all features set", eventstore: expectEventstore( - expectFilter(eventFromEventPusher(feature_v2.NewSetEvent[bool]( + expectFilter(eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, true, ))), expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), ), ), args: args{true}, @@ -132,45 +128,37 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelInstance, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: false, - }, }, }, { name: "all features set, reset, set some feature, cascaded", eventstore: expectEventstore( - expectFilter(eventFromEventPusher(feature_v2.NewSetEvent[bool]( + expectFilter(eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, true, ))), expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( ctx, aggregate, feature_v2.InstanceResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), @@ -197,41 +185,33 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, { name: "all features set, reset, set some feature, not cascaded", eventstore: expectEventstore( expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( ctx, aggregate, feature_v2.InstanceResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, )), @@ -258,10 +238,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, } diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index 8d9c7e1597..55b1c8314b 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -34,8 +33,7 @@ var ( ` FROM (SELECT DISTINCT projections.instances.id, COUNT(*) OVER () FROM projections.instances` + ` LEFT JOIN projections.instance_domains ON projections.instances.id = projections.instance_domains.instance_id) AS f` + ` LEFT JOIN projections.instances ON f.id = projections.instances.id` + - ` LEFT JOIN projections.instance_domains ON f.id = projections.instance_domains.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.instance_domains ON f.id = projections.instance_domains.instance_id` instancesCols = []string{ "count", "id", @@ -64,15 +62,15 @@ func Test_InstancePrepares(t *testing.T) { } tests := []struct { name string - prepare interface{} + prepare any additionalArgs []reflect.Value want want - object interface{} + object any }{ { name: "prepareInstancesQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery(ctx, db) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { + filter, query, scan := prepareInstancesQuery() return query(filter), scan }, want: want{ @@ -86,8 +84,8 @@ func Test_InstancePrepares(t *testing.T) { }, { name: "prepareInstancesQuery one result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery(ctx, db) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { + filter, query, scan := prepareInstancesQuery() return query(filter), scan }, want: want{ @@ -150,8 +148,8 @@ func Test_InstancePrepares(t *testing.T) { }, { name: "prepareInstancesQuery multiple results", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery(ctx, db) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { + filter, query, scan := prepareInstancesQuery() return query(filter), scan }, want: want{ @@ -283,8 +281,8 @@ func Test_InstancePrepares(t *testing.T) { }, { name: "prepareInstancesQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery(ctx, db) + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { + filter, query, scan := prepareInstancesQuery() return query(filter), scan }, want: want{ @@ -304,7 +302,7 @@ func Test_InstancePrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, append(defaultPrepareArgs, tt.additionalArgs...)...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, tt.additionalArgs...) }) } } diff --git a/internal/query/instance_trusted_domain.go b/internal/query/instance_trusted_domain.go index 2847c3969a..8c3fd99987 100644 --- a/internal/query/instance_trusted_domain.go +++ b/internal/query/instance_trusted_domain.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -48,7 +47,7 @@ func (q *Queries) SearchInstanceTrustedDomains(ctx context.Context, queries *Ins ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareInstanceTrustedDomainsQuery(ctx, q.client) + query, scan := prepareInstanceTrustedDomainsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ InstanceTrustedDomainInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -72,7 +71,7 @@ func (q *Queries) queryInstanceTrustedDomains(ctx context.Context, stmt string, return domains, err } -func prepareInstanceTrustedDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*InstanceTrustedDomains, error)) { +func prepareInstanceTrustedDomainsQuery() (sq.SelectBuilder, func(*sql.Rows) (*InstanceTrustedDomains, error)) { return sq.Select( InstanceTrustedDomainCreationDateCol.identifier(), InstanceTrustedDomainChangeDateCol.identifier(), @@ -80,7 +79,7 @@ func prepareInstanceTrustedDomainsQuery(ctx context.Context, db prepareDatabase) InstanceTrustedDomainDomainCol.identifier(), InstanceTrustedDomainInstanceIDCol.identifier(), countColumn.identifier(), - ).From(instanceTrustedDomainsTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(instanceTrustedDomainsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*InstanceTrustedDomains, error) { domains := make([]*InstanceTrustedDomain, 0) diff --git a/internal/query/instance_trusted_domain_test.go b/internal/query/instance_trusted_domain_test.go index 6e3eea027e..518d2edb6b 100644 --- a/internal/query/instance_trusted_domain_test.go +++ b/internal/query/instance_trusted_domain_test.go @@ -16,8 +16,7 @@ var ( ` projections.instance_trusted_domains.domain,` + ` projections.instance_trusted_domains.instance_id,` + ` COUNT(*) OVER ()` + - ` FROM projections.instance_trusted_domains` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.instance_trusted_domains` prepareInstanceTrustedDomainsCols = []string{ "creation_date", "change_date", @@ -151,7 +150,7 @@ func Test_InstanceTrustedDomainPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/introspection_test.go b/internal/query/introspection_test.go index 4346842bf9..92c571ebf9 100644 --- a/internal/query/introspection_test.go +++ b/internal/query/introspection_test.go @@ -91,8 +91,7 @@ func TestQueries_ActiveIntrospectionClientByID(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "userID") diff --git a/internal/query/key.go b/internal/query/key.go index d7475e424b..4831d88654 100644 --- a/internal/query/key.go +++ b/internal/query/key.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query/projection" @@ -182,7 +181,7 @@ func (q *Queries) ActivePublicKeys(ctx context.Context, t time.Time) (keys *Publ ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := preparePublicKeysQuery(ctx, q.client) + query, scan := preparePublicKeysQuery() if t.IsZero() { t = time.Now() } @@ -214,7 +213,7 @@ func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (key ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := preparePrivateKeysQuery(ctx, q.client) + stmt, scan := preparePrivateKeysQuery() if t.IsZero() { t = time.Now() } @@ -244,7 +243,7 @@ func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (key return keys, nil } -func preparePublicKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*PublicKeys, error)) { +func preparePublicKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PublicKeys, error)) { return sq.Select( KeyColID.identifier(), KeyColCreationDate.identifier(), @@ -257,7 +256,7 @@ func preparePublicKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectB KeyPublicColKey.identifier(), countColumn.identifier(), ).From(keyTable.identifier()). - LeftJoin(join(KeyPublicColID, KeyColID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(KeyPublicColID, KeyColID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*PublicKeys, error) { keys := make([]PublicKey, 0) @@ -300,7 +299,7 @@ func preparePublicKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } } -func preparePrivateKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*PrivateKeys, error)) { +func preparePrivateKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PrivateKeys, error)) { return sq.Select( KeyColID.identifier(), KeyColCreationDate.identifier(), @@ -313,7 +312,7 @@ func preparePrivateKeysQuery(ctx context.Context, db prepareDatabase) (sq.Select KeyPrivateColKey.identifier(), countColumn.identifier(), ).From(keyTable.identifier()). - LeftJoin(join(KeyPrivateColID, KeyColID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(KeyPrivateColID, KeyColID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*PrivateKeys, error) { keys := make([]PrivateKey, 0) diff --git a/internal/query/key_test.go b/internal/query/key_test.go index a977bfb58e..7bc029fd7f 100644 --- a/internal/query/key_test.go +++ b/internal/query/key_test.go @@ -36,8 +36,7 @@ var ( ` projections.keys4_public.key,` + ` COUNT(*) OVER ()` + ` FROM projections.keys4` + - ` LEFT JOIN projections.keys4_public ON projections.keys4.id = projections.keys4_public.id AND projections.keys4.instance_id = projections.keys4_public.instance_id` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` LEFT JOIN projections.keys4_public ON projections.keys4.id = projections.keys4_public.id AND projections.keys4.instance_id = projections.keys4_public.instance_id` preparePublicKeysCols = []string{ "id", "creation_date", @@ -62,8 +61,7 @@ var ( ` projections.keys4_private.key,` + ` COUNT(*) OVER ()` + ` FROM projections.keys4` + - ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` ) func Test_KeyPrepares(t *testing.T) { @@ -244,7 +242,7 @@ func Test_KeyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/label_policy.go b/internal/query/label_policy.go index 6dc7b00922..d3952a210a 100644 --- a/internal/query/label_policy.go +++ b/internal/query/label_policy.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -47,7 +46,7 @@ func (q *Queries) ActiveLabelPolicyByOrg(ctx context.Context, orgID string, with ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLabelPolicyQuery(ctx, q.client) + stmt, scan := prepareLabelPolicyQuery() eq := sq.Eq{ LabelPolicyColState.identifier(): domain.LabelPolicyStateActive, LabelPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -80,7 +79,7 @@ func (q *Queries) PreviewLabelPolicyByOrg(ctx context.Context, orgID string) (po ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLabelPolicyQuery(ctx, q.client) + stmt, scan := prepareLabelPolicyQuery() query, args, err := stmt.Where( sq.And{ sq.Or{ @@ -113,7 +112,7 @@ func (q *Queries) DefaultActiveLabelPolicy(ctx context.Context) (policy *LabelPo ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLabelPolicyQuery(ctx, q.client) + stmt, scan := prepareLabelPolicyQuery() query, args, err := stmt.Where(sq.Eq{ LabelPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), LabelPolicyColState.identifier(): domain.LabelPolicyStateActive, @@ -136,7 +135,7 @@ func (q *Queries) DefaultPreviewLabelPolicy(ctx context.Context) (policy *LabelP ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLabelPolicyQuery(ctx, q.client) + stmt, scan := prepareLabelPolicyQuery() query, args, err := stmt.Where(sq.Eq{ LabelPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), LabelPolicyColState.identifier(): domain.LabelPolicyStatePreview, @@ -240,7 +239,7 @@ var ( } ) -func prepareLabelPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*LabelPolicy, error)) { +func prepareLabelPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LabelPolicy, error)) { return sq.Select( LabelPolicyColCreationDate.identifier(), LabelPolicyColChangeDate.identifier(), @@ -270,7 +269,7 @@ func prepareLabelPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select LabelPolicyColDarkLogoURL.identifier(), LabelPolicyColDarkIconURL.identifier(), ). - From(labelPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(labelPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*LabelPolicy, error) { policy := new(LabelPolicy) diff --git a/internal/query/lockout_policy.go b/internal/query/lockout_policy.go index be4b162785..078c743413 100644 --- a/internal/query/lockout_policy.go +++ b/internal/query/lockout_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -98,7 +97,7 @@ func (q *Queries) LockoutPolicyByOrg(ctx context.Context, shouldTriggerBulk bool LockoutColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - stmt, scan := prepareLockoutPolicyQuery(ctx, q.client) + stmt, scan := prepareLockoutPolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -124,7 +123,7 @@ func (q *Queries) DefaultLockoutPolicy(ctx context.Context) (policy *LockoutPoli ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareLockoutPolicyQuery(ctx, q.client) + stmt, scan := prepareLockoutPolicyQuery() query, args, err := stmt.Where(sq.Eq{ LockoutColID.identifier(): authz.GetInstance(ctx).InstanceID(), LockoutColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -142,7 +141,7 @@ func (q *Queries) DefaultLockoutPolicy(ctx context.Context) (policy *LockoutPoli return policy, err } -func prepareLockoutPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*LockoutPolicy, error)) { +func prepareLockoutPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LockoutPolicy, error)) { return sq.Select( LockoutColID.identifier(), LockoutColSequence.identifier(), @@ -155,7 +154,7 @@ func prepareLockoutPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele LockoutColIsDefault.identifier(), LockoutColState.identifier(), ). - From(lockoutTable.identifier() + db.Timetravel(call.Took(ctx))). + From(lockoutTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*LockoutPolicy, error) { policy := new(LockoutPolicy) diff --git a/internal/query/lockout_policy_test.go b/internal/query/lockout_policy_test.go index 2805ef8fdc..0c0a9f04eb 100644 --- a/internal/query/lockout_policy_test.go +++ b/internal/query/lockout_policy_test.go @@ -23,8 +23,7 @@ var ( ` projections.lockout_policies3.max_otp_attempts,` + ` projections.lockout_policies3.is_default,` + ` projections.lockout_policies3.state` + - ` FROM projections.lockout_policies3` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.lockout_policies3` prepareLockoutPolicyCols = []string{ "id", @@ -123,7 +122,7 @@ func Test_LockoutPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/login_policy.go b/internal/query/login_policy.go index 5ab54cfa55..946dbb04de 100644 --- a/internal/query/login_policy.go +++ b/internal/query/login_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -183,7 +182,7 @@ func (q *Queries) LoginPolicyByID(ctx context.Context, shouldTriggerBulk bool, o eq[LoginPolicyColumnOwnerRemoved.identifier()] = false } - query, scan := prepareLoginPolicyQuery(ctx, q.client) + query, scan := prepareLoginPolicyQuery() stmt, args, err := query.Where( sq.And{ eq, @@ -219,7 +218,7 @@ func (q *Queries) DefaultLoginPolicy(ctx context.Context) (policy *LoginPolicy, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicyQuery(ctx, q.client) + query, scan := prepareLoginPolicyQuery() stmt, args, err := query.Where(sq.Eq{ LoginPolicyColumnOrgID.identifier(): authz.GetInstance(ctx).InstanceID(), LoginPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -242,7 +241,7 @@ func (q *Queries) SecondFactorsByOrg(ctx context.Context, orgID string) (factors ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicy2FAsQuery(ctx, q.client) + query, scan := prepareLoginPolicy2FAsQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{ @@ -278,7 +277,7 @@ func (q *Queries) DefaultSecondFactors(ctx context.Context) (factors *SecondFact ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicy2FAsQuery(ctx, q.client) + query, scan := prepareLoginPolicy2FAsQuery() stmt, args, err := query.Where(sq.Eq{ LoginPolicyColumnOrgID.identifier(): authz.GetInstance(ctx).InstanceID(), LoginPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -302,7 +301,7 @@ func (q *Queries) MultiFactorsByOrg(ctx context.Context, orgID string) (factors ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicyMFAsQuery(ctx, q.client) + query, scan := prepareLoginPolicyMFAsQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{ @@ -338,7 +337,7 @@ func (q *Queries) DefaultMultiFactors(ctx context.Context) (factors *MultiFactor ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareLoginPolicyMFAsQuery(ctx, q.client) + query, scan := prepareLoginPolicyMFAsQuery() stmt, args, err := query.Where(sq.Eq{ LoginPolicyColumnOrgID.identifier(): authz.GetInstance(ctx).InstanceID(), LoginPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -358,7 +357,7 @@ func (q *Queries) DefaultMultiFactors(ctx context.Context) (factors *MultiFactor return factors, err } -func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, error)) { +func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, error)) { return sq.Select( LoginPolicyColumnOrgID.identifier(), LoginPolicyColumnCreationDate.identifier(), @@ -384,7 +383,7 @@ func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select LoginPolicyColumnMFAInitSkipLifetime.identifier(), LoginPolicyColumnSecondFactorCheckLifetime.identifier(), LoginPolicyColumnMultiFactorCheckLifetime.identifier(), - ).From(loginPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(loginPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*LoginPolicy, error) { p := new(LoginPolicy) @@ -428,10 +427,10 @@ func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select } } -func prepareLoginPolicy2FAsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SecondFactors, error)) { +func prepareLoginPolicy2FAsQuery() (sq.SelectBuilder, func(*sql.Row) (*SecondFactors, error)) { return sq.Select( LoginPolicyColumnSecondFactors.identifier(), - ).From(loginPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(loginPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SecondFactors, error) { p := new(SecondFactors) @@ -450,10 +449,10 @@ func prepareLoginPolicy2FAsQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareLoginPolicyMFAsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*MultiFactors, error)) { +func prepareLoginPolicyMFAsQuery() (sq.SelectBuilder, func(*sql.Row) (*MultiFactors, error)) { return sq.Select( LoginPolicyColumnMultiFactors.identifier(), - ).From(loginPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(loginPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*MultiFactors, error) { p := new(MultiFactors) diff --git a/internal/query/login_policy_test.go b/internal/query/login_policy_test.go index f64c94e275..792f30517a 100644 --- a/internal/query/login_policy_test.go +++ b/internal/query/login_policy_test.go @@ -39,8 +39,7 @@ var ( ` projections.login_policies5.mfa_init_skip_lifetime,` + ` projections.login_policies5.second_factor_check_lifetime,` + ` projections.login_policies5.multi_factor_check_lifetime` + - ` FROM projections.login_policies5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.login_policies5` loginPolicyCols = []string{ "aggregate_id", "creation_date", @@ -69,15 +68,13 @@ var ( } prepareLoginPolicy2FAsStmt = `SELECT projections.login_policies5.second_factors` + - ` FROM projections.login_policies5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.login_policies5` prepareLoginPolicy2FAsCols = []string{ "second_factors", } prepareLoginPolicyMFAsStmt = `SELECT projections.login_policies5.multi_factors` + - ` FROM projections.login_policies5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.login_policies5` prepareLoginPolicyMFAsCols = []string{ "multi_factors", } @@ -331,7 +328,7 @@ func Test_LoginPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/mail_template.go b/internal/query/mail_template.go index 9d5ff83162..518ab7aec5 100644 --- a/internal/query/mail_template.go +++ b/internal/query/mail_template.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -74,7 +73,7 @@ func (q *Queries) MailTemplateByOrg(ctx context.Context, orgID string, withOwner ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareMailTemplateQuery(ctx, q.client) + stmt, scan := prepareMailTemplateQuery() eq := sq.Eq{MailTemplateColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} if !withOwnerRemoved { eq[MailTemplateColOwnerRemoved.identifier()] = false @@ -104,7 +103,7 @@ func (q *Queries) DefaultMailTemplate(ctx context.Context) (template *MailTempla ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareMailTemplateQuery(ctx, q.client) + stmt, scan := prepareMailTemplateQuery() query, args, err := stmt.Where(sq.Eq{ MailTemplateColAggregateID.identifier(): authz.GetInstance(ctx).InstanceID(), MailTemplateColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -122,7 +121,7 @@ func (q *Queries) DefaultMailTemplate(ctx context.Context) (template *MailTempla return template, err } -func prepareMailTemplateQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*MailTemplate, error)) { +func prepareMailTemplateQuery() (sq.SelectBuilder, func(*sql.Row) (*MailTemplate, error)) { return sq.Select( MailTemplateColAggregateID.identifier(), MailTemplateColSequence.identifier(), @@ -132,7 +131,7 @@ func prepareMailTemplateQuery(ctx context.Context, db prepareDatabase) (sq.Selec MailTemplateColIsDefault.identifier(), MailTemplateColState.identifier(), ). - From(mailTemplateTable.identifier() + db.Timetravel(call.Took(ctx))). + From(mailTemplateTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*MailTemplate, error) { policy := new(MailTemplate) diff --git a/internal/query/message_text.go b/internal/query/message_text.go index cb524d289a..3d9a2c33b3 100644 --- a/internal/query/message_text.go +++ b/internal/query/message_text.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "os" "time" @@ -15,7 +15,6 @@ import ( "sigs.k8s.io/yaml" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query/projection" @@ -131,7 +130,7 @@ func (q *Queries) DefaultMessageText(ctx context.Context) (text *MessageText, er ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareMessageTextQuery(ctx, q.client) + stmt, scan := prepareMessageTextQuery() query, args, err := stmt.Where(sq.Eq{ MessageTextColAggregateID.identifier(): authz.GetInstance(ctx).InstanceID(), MessageTextColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -167,7 +166,7 @@ func (q *Queries) CustomMessageTextByTypeAndLanguage(ctx context.Context, aggreg ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareMessageTextQuery(ctx, q.client) + stmt, scan := prepareMessageTextQuery() eq := sq.Eq{ MessageTextColLanguage.identifier(): language, MessageTextColType.identifier(): messageType, @@ -249,7 +248,7 @@ func (q *Queries) readNotificationTextMessages(ctx context.Context, language str return contents, nil } -func prepareMessageTextQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*MessageText, error)) { +func prepareMessageTextQuery() (sq.SelectBuilder, func(*sql.Row) (*MessageText, error)) { return sq.Select( MessageTextColAggregateID.identifier(), MessageTextColSequence.identifier(), @@ -266,7 +265,7 @@ func prepareMessageTextQuery(ctx context.Context, db prepareDatabase) (sq.Select MessageTextColButtonText.identifier(), MessageTextColFooter.identifier(), ). - From(messageTextTable.identifier() + db.Timetravel(call.Took(ctx))). + From(messageTextTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*MessageText, error) { msg := new(MessageText) @@ -320,7 +319,7 @@ func (q *Queries) readTranslationFile(namespace i18n.Namespace, filename string) if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-93njw", "Errors.TranslationFile.ReadError") } - contents, err := ioutil.ReadAll(r) + contents, err := io.ReadAll(r) if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-l0fse", "Errors.TranslationFile.ReadError") } diff --git a/internal/query/message_text_test.go b/internal/query/message_text_test.go index 09df5dcd83..4e78f4813d 100644 --- a/internal/query/message_text_test.go +++ b/internal/query/message_text_test.go @@ -29,8 +29,7 @@ var ( ` projections.message_texts2.text,` + ` projections.message_texts2.button_text,` + ` projections.message_texts2.footer_text` + - ` FROM projections.message_texts2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.message_texts2` prepareMessgeTextCols = []string{ "aggregate_id", "sequence", @@ -140,7 +139,7 @@ func Test_MessageTextPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/milestone.go b/internal/query/milestone.go index 4277b8e68a..f6d2c47de0 100644 --- a/internal/query/milestone.go +++ b/internal/query/milestone.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/milestone" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -68,13 +67,15 @@ var ( func (q *Queries) SearchMilestones(ctx context.Context, instanceIDs []string, queries *MilestonesSearchQueries) (milestones *Milestones, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareMilestonesQuery(ctx, q.client) + query, scan := prepareMilestonesQuery() if len(instanceIDs) == 0 { instanceIDs = []string{authz.GetInstance(ctx).InstanceID()} } stmt, args, err := queries.toQuery(query).Where( - sq.Eq{MilestoneInstanceIDColID.identifier(): instanceIDs}, - sq.Eq{InstanceDomainIsPrimaryCol.identifier(): true}, + sq.Eq{ + MilestoneInstanceIDColID.identifier(): instanceIDs, + InstanceDomainIsPrimaryCol.identifier(): true, + }, ).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-A9i5k", "Errors.Query.SQLStatement") @@ -89,10 +90,9 @@ func (q *Queries) SearchMilestones(ctx context.Context, instanceIDs []string, qu milestones.State, err = q.latestState(ctx, milestonesTable) return milestones, err - } -func prepareMilestonesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Milestones, error)) { +func prepareMilestonesQuery() (sq.SelectBuilder, func(*sql.Rows) (*Milestones, error)) { return sq.Select( MilestoneInstanceIDColID.identifier(), InstanceDomainDomainCol.identifier(), @@ -101,7 +101,7 @@ func prepareMilestonesQuery(ctx context.Context, db prepareDatabase) (sq.SelectB MilestoneTypeColID.identifier(), countColumn.identifier(), ). - From(milestonesTable.identifier() + db.Timetravel(call.Took(ctx))). + From(milestonesTable.identifier()). LeftJoin(join(InstanceDomainInstanceIDCol, MilestoneInstanceIDColID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Milestones, error) { diff --git a/internal/query/milestone_test.go b/internal/query/milestone_test.go index ee99474ec2..027ebca48c 100644 --- a/internal/query/milestone_test.go +++ b/internal/query/milestone_test.go @@ -17,7 +17,7 @@ var ( projections.milestones3.last_pushed_date, projections.milestones3.type, COUNT(*) OVER () - FROM projections.milestones3 AS OF SYSTEM TIME '-1 ms' + FROM projections.milestones3 LEFT JOIN projections.instance_domains ON projections.milestones3.instance_id = projections.instance_domains.instance_id `) @@ -184,7 +184,7 @@ func Test_MilestonesPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/notification_policy.go b/internal/query/notification_policy.go index f3878e7987..45779762d9 100644 --- a/internal/query/notification_policy.go +++ b/internal/query/notification_policy.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -93,7 +92,7 @@ func (q *Queries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk if !withOwnerRemoved { eq[NotificationPolicyColOwnerRemoved.identifier()] = false } - stmt, scan := prepareNotificationPolicyQuery(ctx, q.client) + stmt, scan := prepareNotificationPolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -127,7 +126,7 @@ func (q *Queries) DefaultNotificationPolicy(ctx context.Context, shouldTriggerBu } } - stmt, scan := prepareNotificationPolicyQuery(ctx, q.client) + stmt, scan := prepareNotificationPolicyQuery() query, args, err := stmt.Where(sq.Eq{ NotificationPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), NotificationPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -145,7 +144,7 @@ func (q *Queries) DefaultNotificationPolicy(ctx context.Context, shouldTriggerBu return policy, err } -func prepareNotificationPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*NotificationPolicy, error)) { +func prepareNotificationPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*NotificationPolicy, error)) { return sq.Select( NotificationPolicyColID.identifier(), NotificationPolicyColSequence.identifier(), @@ -156,7 +155,7 @@ func prepareNotificationPolicyQuery(ctx context.Context, db prepareDatabase) (sq NotificationPolicyColIsDefault.identifier(), NotificationPolicyColState.identifier(), ). - From(notificationPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(notificationPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*NotificationPolicy, error) { policy := new(NotificationPolicy) diff --git a/internal/query/notification_policy_test.go b/internal/query/notification_policy_test.go index d755bdc544..bbb40d4e5b 100644 --- a/internal/query/notification_policy_test.go +++ b/internal/query/notification_policy_test.go @@ -21,8 +21,7 @@ var ( ` projections.notification_policies.password_change,` + ` projections.notification_policies.is_default,` + ` projections.notification_policies.state` + - ` FROM projections.notification_policies` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` FROM projections.notification_policies`) notificationPolicyCols = []string{ "id", "sequence", @@ -114,7 +113,7 @@ func Test_NotificationPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/notification_provider.go b/internal/query/notification_provider.go index b2038c603d..fa48e42c9b 100644 --- a/internal/query/notification_provider.go +++ b/internal/query/notification_provider.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -74,7 +73,7 @@ func (q *Queries) NotificationProviderByIDAndType(ctx context.Context, aggID str ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareDebugNotificationProviderQuery(ctx, q.client) + query, scan := prepareDebugNotificationProviderQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{NotificationProviderColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}, @@ -97,7 +96,7 @@ func (q *Queries) NotificationProviderByIDAndType(ctx context.Context, aggID str return provider, err } -func prepareDebugNotificationProviderQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*DebugNotificationProvider, error)) { +func prepareDebugNotificationProviderQuery() (sq.SelectBuilder, func(*sql.Row) (*DebugNotificationProvider, error)) { return sq.Select( NotificationProviderColumnAggID.identifier(), NotificationProviderColumnCreationDate.identifier(), @@ -107,7 +106,7 @@ func prepareDebugNotificationProviderQuery(ctx context.Context, db prepareDataba NotificationProviderColumnState.identifier(), NotificationProviderColumnType.identifier(), NotificationProviderColumnCompact.identifier(), - ).From(notificationProviderTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(notificationProviderTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*DebugNotificationProvider, error) { p := new(DebugNotificationProvider) diff --git a/internal/query/notification_provider_test.go b/internal/query/notification_provider_test.go index 2fce31e118..a2c88ccbcb 100644 --- a/internal/query/notification_provider_test.go +++ b/internal/query/notification_provider_test.go @@ -21,8 +21,7 @@ var ( ` projections.notification_providers.state,` + ` projections.notification_providers.provider_type,` + ` projections.notification_providers.compact` + - ` FROM projections.notification_providers` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.notification_providers` prepareNotificationProviderCols = []string{ "aggregate_id", "creation_date", @@ -114,7 +113,7 @@ func Test_NotificationProviderPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/oidc_client_test.go b/internal/query/oidc_client_test.go index bb0890bff3..826e5071db 100644 --- a/internal/query/oidc_client_test.go +++ b/internal/query/oidc_client_test.go @@ -4,6 +4,7 @@ import ( "database/sql" "database/sql/driver" _ "embed" + "net/url" "regexp" "testing" @@ -19,6 +20,8 @@ import ( var ( //go:embed testdata/oidc_client_jwt.json testdataOidcClientJWT string + //go:embed testdata/oidc_client_jwt_loginversion.json + testdataOidcClientJWTLoginVersion string //go:embed testdata/oidc_client_public.json testdataOidcClientPublic string //go:embed testdata/oidc_client_public_old_id.json @@ -91,6 +94,44 @@ low2kyJov38V4Uk2I8kuXpLcnrpw5Tio2ooiUE27b0vHZqBKOei9Uo88qCrn3EKx }, }, }, + { + name: "jwt client, login version", + mock: mockQuery(expQuery, cols, []driver.Value{testdataOidcClientJWTLoginVersion}, "instanceID", "clientID", true), + want: &OIDCClient{ + InstanceID: "230690539048009730", + AppID: "236647088211886082", + State: domain.AppStateActive, + ClientID: "236647088211951618", + HashedSecret: "", + RedirectURIs: []string{"http://localhost:9999/auth/callback"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode, domain.OIDCGrantTypeRefreshToken}, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypePrivateKeyJWT, + PostLogoutRedirectURIs: []string{"https://example.com/logout"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AccessTokenRoleAssertion: true, + IDTokenRoleAssertion: true, + IDTokenUserinfoAssertion: true, + ClockSkew: 1000000000, + AdditionalOrigins: []string{"https://example.com"}, + ProjectID: "236645808328409090", + ProjectRoleAssertion: true, + PublicKeys: map[string][]byte{"236647201860747266": []byte(pubkey)}, + ProjectRoleKeys: []string{"role1", "role2"}, + Settings: &OIDCSettings{ + AccessTokenLifetime: 43200000000000, + IdTokenLifetime: 43200000000000, + }, + LoginVersion: domain.LoginVersion1, + LoginBaseURI: func() *URL { + ret, _ := url.Parse("https://test.com/login") + retURL := URL(*ret) + return &retURL + }(), + }, + }, { name: "public client", mock: mockQuery(expQuery, cols, []driver.Value{testdataOidcClientPublic}, "instanceID", "clientID", true), @@ -227,8 +268,7 @@ low2kyJov38V4Uk2I8kuXpLcnrpw5Tio2ooiUE27b0vHZqBKOei9Uo88qCrn3EKx execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") diff --git a/internal/query/oidc_settings.go b/internal/query/oidc_settings.go index 32cbc32429..bdd21cfd15 100644 --- a/internal/query/oidc_settings.go +++ b/internal/query/oidc_settings.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -79,7 +78,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) ( ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareOIDCSettingsQuery(ctx, q.client) + stmt, scan := prepareOIDCSettingsQuery() query, args, err := stmt.Where(sq.Eq{ OIDCSettingsColumnAggregateID.identifier(): aggregateID, OIDCSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -95,7 +94,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) ( return settings, err } -func prepareOIDCSettingsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*OIDCSettings, error)) { +func prepareOIDCSettingsQuery() (sq.SelectBuilder, func(*sql.Row) (*OIDCSettings, error)) { return sq.Select( OIDCSettingsColumnAggregateID.identifier(), OIDCSettingsColumnCreationDate.identifier(), @@ -106,7 +105,7 @@ func prepareOIDCSettingsQuery(ctx context.Context, db prepareDatabase) (sq.Selec OIDCSettingsColumnIdTokenLifetime.identifier(), OIDCSettingsColumnRefreshTokenIdleExpiration.identifier(), OIDCSettingsColumnRefreshTokenExpiration.identifier()). - From(oidcSettingsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(oidcSettingsTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*OIDCSettings, error) { oidcSettings := new(OIDCSettings) diff --git a/internal/query/oidc_settings_test.go b/internal/query/oidc_settings_test.go index bdb5cb96ec..625c16f34c 100644 --- a/internal/query/oidc_settings_test.go +++ b/internal/query/oidc_settings_test.go @@ -22,8 +22,7 @@ var ( ` projections.oidc_settings2.id_token_lifetime,` + ` projections.oidc_settings2.refresh_token_idle_expiration,` + ` projections.oidc_settings2.refresh_token_expiration` + - ` FROM projections.oidc_settings2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.oidc_settings2` prepareOIDCSettingsCols = []string{ "aggregate_id", "creation_date", @@ -118,7 +117,7 @@ func Test_OIDCConfigsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/org.go b/internal/query/org.go index e5bfc5140f..dfe90ad9f8 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" domain_pkg "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/feature" @@ -94,6 +93,18 @@ func orgsCheckPermission(ctx context.Context, orgs *Orgs, permissionCheck domain ) } +func orgsPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + OrgColumnID, + domain_pkg.PermissionOrgRead, + ) + return query.JoinClause(join, args...) +} + type OrgSearchQueries struct { SearchRequest Queries []SearchQuery @@ -164,7 +175,7 @@ func (q *Queries) oldOrgByID(ctx context.Context, shouldTriggerBulk bool, id str traceSpan.EndWithError(err) } - stmt, scan := prepareOrgQuery(ctx, q.client) + stmt, scan := prepareOrgQuery() query, args, err := stmt.Where(sq.Eq{ OrgColumnID.identifier(): id, OrgColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -189,7 +200,7 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O return org, nil } - stmt, scan := prepareOrgQuery(ctx, q.client) + stmt, scan := prepareOrgQuery() query, args, err := stmt.Where(sq.Eq{ OrgColumnDomain.identifier(): domain, OrgColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -213,7 +224,7 @@ func (q *Queries) OrgByVerifiedDomain(ctx context.Context, domain string) (org * ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareOrgWithDomainsQuery(ctx, q.client) + stmt, scan := prepareOrgWithDomainsQuery() query, args, err := stmt.Where(sq.Eq{ OrgDomainDomainCol.identifier(): domain, OrgDomainIsVerifiedCol.identifier(): true, @@ -237,7 +248,7 @@ func (q *Queries) IsOrgUnique(ctx context.Context, name, domain string) (isUniqu if name == "" && domain == "" { return false, zerrors.ThrowInvalidArgument(nil, "QUERY-DGqfd", "Errors.Query.InvalidRequest") } - query, scan := prepareOrgUniqueQuery(ctx, q.client) + query, scan := prepareOrgUniqueQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{ @@ -284,21 +295,23 @@ func (q *Queries) ExistsOrg(ctx context.Context, id, domain string) (verifiedID } func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries, permissionCheck domain_pkg.PermissionCheck) (*Orgs, error) { - orgs, err := q.searchOrgs(ctx, queries) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + orgs, err := q.searchOrgs(ctx, queries, permissionCheckV2) if err != nil { return nil, err } - if permissionCheck != nil { + if permissionCheck != nil && !permissionCheckV2 { orgsCheckPermission(ctx, orgs, permissionCheck) } return orgs, nil } -func (q *Queries) searchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) { +func (q *Queries) searchOrgs(ctx context.Context, queries *OrgSearchQueries, permissionCheckV2 bool) (orgs *Orgs, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareOrgsQuery(ctx, q.client) + query, scan := prepareOrgsQuery() + query = orgsPermissionCheckV2(ctx, query, permissionCheckV2) stmt, args, err := queries.toQuery(query). Where(sq.And{ sq.Eq{ @@ -361,7 +374,7 @@ func NewOrgIDsSearchQuery(ids ...string) (SearchQuery, error) { return NewListQuery(OrgColumnID, list, ListIn) } -func prepareOrgsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Orgs, error)) { +func prepareOrgsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Orgs, error)) { return sq.Select( OrgColumnID.identifier(), OrgColumnCreationDate.identifier(), @@ -372,7 +385,7 @@ func prepareOrgsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder OrgColumnName.identifier(), OrgColumnDomain.identifier(), countColumn.identifier()). - From(orgsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(orgsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Orgs, error) { orgs := make([]*Org, 0) @@ -409,42 +422,7 @@ func prepareOrgsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder } } -func prepareOrgQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { - return sq.Select( - OrgColumnID.identifier(), - OrgColumnCreationDate.identifier(), - OrgColumnChangeDate.identifier(), - OrgColumnResourceOwner.identifier(), - OrgColumnState.identifier(), - OrgColumnSequence.identifier(), - OrgColumnName.identifier(), - OrgColumnDomain.identifier(), - ). - From(orgsTable.identifier() + db.Timetravel(call.Took(ctx))). - PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*Org, error) { - o := new(Org) - err := row.Scan( - &o.ID, - &o.CreationDate, - &o.ChangeDate, - &o.ResourceOwner, - &o.State, - &o.Sequence, - &o.Name, - &o.Domain, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-iTTGJ", "Errors.Org.NotFound") - } - return nil, zerrors.ThrowInternal(err, "QUERY-pWS5H", "Errors.Internal") - } - return o, nil - } -} - -func prepareOrgWithDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { +func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { return sq.Select( OrgColumnID.identifier(), OrgColumnCreationDate.identifier(), @@ -456,7 +434,6 @@ func prepareOrgWithDomainsQuery(ctx context.Context, db prepareDatabase) (sq.Sel OrgColumnDomain.identifier(), ). From(orgsTable.identifier()). - LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Org, error) { o := new(Org) @@ -480,10 +457,46 @@ func prepareOrgWithDomainsQuery(ctx context.Context, db prepareDatabase) (sq.Sel } } -func prepareOrgUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (bool, error)) { +func prepareOrgWithDomainsQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { + return sq.Select( + OrgColumnID.identifier(), + OrgColumnCreationDate.identifier(), + OrgColumnChangeDate.identifier(), + OrgColumnResourceOwner.identifier(), + OrgColumnState.identifier(), + OrgColumnSequence.identifier(), + OrgColumnName.identifier(), + OrgColumnDomain.identifier(), + ). + From(orgsTable.identifier()). + LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Org, error) { + o := new(Org) + err := row.Scan( + &o.ID, + &o.CreationDate, + &o.ChangeDate, + &o.ResourceOwner, + &o.State, + &o.Sequence, + &o.Name, + &o.Domain, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-iTTGJ", "Errors.Org.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-pWS5H", "Errors.Internal") + } + return o, nil + } +} + +func prepareOrgUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { return sq.Select(uniqueColumn.identifier()). From(orgsTable.identifier()). - LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (isUnique bool, err error) { err = row.Scan(&isUnique) diff --git a/internal/query/org_domain.go b/internal/query/org_domain.go index 595ba897d0..ed0dba9c17 100644 --- a/internal/query/org_domain.go +++ b/internal/query/org_domain.go @@ -8,7 +8,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -60,7 +59,7 @@ func (q *Queries) SearchOrgDomains(ctx context.Context, queries *OrgDomainSearch ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareDomainsQuery(ctx, q.client) + query, scan := prepareDomainsQuery() eq := sq.Eq{OrgDomainInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} if !withOwnerRemoved { eq[OrgDomainOwnerRemovedCol.identifier()] = false @@ -82,7 +81,7 @@ func (q *Queries) SearchOrgDomains(ctx context.Context, queries *OrgDomainSearch return domains, err } -func prepareDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Domains, error)) { +func prepareDomainsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Domains, error)) { return sq.Select( OrgDomainCreationDateCol.identifier(), OrgDomainChangeDateCol.identifier(), @@ -93,7 +92,7 @@ func prepareDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil OrgDomainIsPrimaryCol.identifier(), OrgDomainValidationTypeCol.identifier(), countColumn.identifier(), - ).From(orgDomainsTable.identifier() + db.Timetravel(call.Took(ctx))). + ).From(orgDomainsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Domains, error) { domains := make([]*Domain, 0) diff --git a/internal/query/org_domain_test.go b/internal/query/org_domain_test.go index 5757eda657..6668528241 100644 --- a/internal/query/org_domain_test.go +++ b/internal/query/org_domain_test.go @@ -21,8 +21,7 @@ var ( ` projections.org_domains2.is_primary,` + ` projections.org_domains2.validation_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.org_domains2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.org_domains2` prepareOrgDomainsCols = []string{ "id", "creation_date", @@ -177,7 +176,7 @@ func Test_OrgDomainPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/org_member.go b/internal/query/org_member.go index 4daa31d341..a85c5d5f6a 100644 --- a/internal/query/org_member.go +++ b/internal/query/org_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -73,7 +72,7 @@ func (q *Queries) OrgMembers(ctx context.Context, queries *OrgMembersQuery) (mem ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareOrgMembersQuery(ctx, q.client) + query, scan := prepareOrgMembersQuery() eq := sq.Eq{OrgMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -97,7 +96,7 @@ func (q *Queries) OrgMembers(ctx context.Context, queries *OrgMembersQuery) (mem return members, err } -func prepareOrgMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareOrgMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( OrgMemberCreationDate.identifier(), OrgMemberChangeDate.identifier(), @@ -119,7 +118,7 @@ func prepareOrgMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectB LeftJoin(join(HumanUserIDCol, OrgMemberUserID)). LeftJoin(join(MachineUserIDCol, OrgMemberUserID)). LeftJoin(join(UserIDCol, OrgMemberUserID)). - LeftJoin(join(LoginNameUserIDCol, OrgMemberUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, OrgMemberUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/org_member_test.go b/internal/query/org_member_test.go index 8433c338ee..cb0b64d55f 100644 --- a/internal/query/org_member_test.go +++ b/internal/query/org_member_test.go @@ -43,7 +43,6 @@ var ( "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + - "AS OF SYSTEM TIME '-1 ms' " + "WHERE projections.login_names3.is_primary = $1") orgMembersColumns = []string{ "creation_date", @@ -299,7 +298,7 @@ func Test_OrgMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index 1ce95e2880..84b204de2b 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -90,7 +89,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo traceSpan.EndWithError(err) } - query, scan := prepareOrgMetadataQuery(ctx, q.client) + query, scan := prepareOrgMetadataQuery() for _, q := range queries { query = q.toQuery(query) } @@ -131,7 +130,7 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, if !withOwnerRemoved { eq[OrgMetadataOwnerRemovedCol.identifier()] = false } - query, scan := prepareOrgMetadataListQuery(ctx, q.client) + query, scan := prepareOrgMetadataListQuery() stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatment") @@ -174,7 +173,7 @@ func NewOrgMetadataKeySearchQuery(value string, comparison TextComparison) (Sear return NewTextQuery(OrgMetadataKeyCol, value, comparison) } -func prepareOrgMetadataQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, error)) { +func prepareOrgMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, error)) { return sq.Select( OrgMetadataCreationDateCol.identifier(), OrgMetadataChangeDateCol.identifier(), @@ -183,7 +182,7 @@ func prepareOrgMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Select OrgMetadataKeyCol.identifier(), OrgMetadataValueCol.identifier(), ). - From(orgMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(orgMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*OrgMetadata, error) { m := new(OrgMetadata) @@ -206,7 +205,7 @@ func prepareOrgMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Select } } -func prepareOrgMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*OrgMetadataList, error)) { +func prepareOrgMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*OrgMetadataList, error)) { return sq.Select( OrgMetadataCreationDateCol.identifier(), OrgMetadataChangeDateCol.identifier(), @@ -215,7 +214,7 @@ func prepareOrgMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.Se OrgMetadataKeyCol.identifier(), OrgMetadataValueCol.identifier(), countColumn.identifier()). - From(orgMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(orgMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*OrgMetadataList, error) { metadata := make([]*OrgMetadata, 0) diff --git a/internal/query/org_metadata_test.go b/internal/query/org_metadata_test.go index 0225ef1c2a..666fddd0fd 100644 --- a/internal/query/org_metadata_test.go +++ b/internal/query/org_metadata_test.go @@ -18,8 +18,7 @@ var ( ` projections.org_metadata2.sequence,` + ` projections.org_metadata2.key,` + ` projections.org_metadata2.value` + - ` FROM projections.org_metadata2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.org_metadata2` orgMetadataCols = []string{ "creation_date", "change_date", @@ -35,8 +34,7 @@ var ( ` projections.org_metadata2.key,` + ` projections.org_metadata2.value,` + ` COUNT(*) OVER ()` + - ` FROM projections.org_metadata2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.org_metadata2` orgMetadataListCols = []string{ "creation_date", "change_date", @@ -244,7 +242,7 @@ func Test_OrgMetadataPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/org_test.go b/internal/query/org_test.go index db41f9ffd1..d704d2901a 100644 --- a/internal/query/org_test.go +++ b/internal/query/org_test.go @@ -19,7 +19,7 @@ import ( ) var ( - orgUniqueQuery = "SELECT COUNT(*) = 0 FROM projections.orgs1 LEFT JOIN projections.org_domains2 ON projections.orgs1.id = projections.org_domains2.org_id AND projections.orgs1.instance_id = projections.org_domains2.instance_id AS OF SYSTEM TIME '-1 ms' WHERE (projections.org_domains2.is_verified = $1 AND projections.orgs1.instance_id = $2 AND (projections.org_domains2.domain ILIKE $3 OR projections.orgs1.name ILIKE $4) AND projections.orgs1.org_state <> $5)" + orgUniqueQuery = "SELECT COUNT(*) = 0 FROM projections.orgs1 LEFT JOIN projections.org_domains2 ON projections.orgs1.id = projections.org_domains2.org_id AND projections.orgs1.instance_id = projections.org_domains2.instance_id WHERE (projections.org_domains2.is_verified = $1 AND projections.orgs1.instance_id = $2 AND (projections.org_domains2.domain ILIKE $3 OR projections.orgs1.name ILIKE $4) AND projections.orgs1.org_state <> $5)" orgUniqueCols = []string{"is_unique"} prepareOrgsQueryStmt = `SELECT projections.orgs1.id,` + @@ -31,8 +31,7 @@ var ( ` projections.orgs1.name,` + ` projections.orgs1.primary_domain,` + ` COUNT(*) OVER ()` + - ` FROM projections.orgs1` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` FROM projections.orgs1` prepareOrgsQueryCols = []string{ "id", "creation_date", @@ -53,8 +52,7 @@ var ( ` projections.orgs1.sequence,` + ` projections.orgs1.name,` + ` projections.orgs1.primary_domain` + - ` FROM projections.orgs1` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` FROM projections.orgs1` prepareOrgQueryCols = []string{ "id", "creation_date", @@ -68,8 +66,7 @@ var ( prepareOrgUniqueStmt = `SELECT COUNT(*) = 0` + ` FROM projections.orgs1` + - ` LEFT JOIN projections.org_domains2 ON projections.orgs1.id = projections.org_domains2.org_id AND projections.orgs1.instance_id = projections.org_domains2.instance_id` + - ` AS OF SYSTEM TIME '-1 ms' ` + ` LEFT JOIN projections.org_domains2 ON projections.orgs1.id = projections.org_domains2.org_id AND projections.orgs1.instance_id = projections.org_domains2.instance_id` prepareOrgUniqueCols = []string{ "count", } @@ -330,7 +327,7 @@ func Test_OrgPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -421,8 +418,7 @@ func TestQueries_IsOrgUnique(t *testing.T) { t.Run(tt.name, func(t *testing.T) { q := &Queries{ client: &database.DB{ - DB: client, - Database: new(prepareDB), + DB: client, }, } diff --git a/internal/query/password_age_policy.go b/internal/query/password_age_policy.go index 15b1b248c8..f5f0491d7b 100644 --- a/internal/query/password_age_policy.go +++ b/internal/query/password_age_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -97,7 +96,7 @@ func (q *Queries) PasswordAgePolicyByOrg(ctx context.Context, shouldTriggerBulk if !withOwnerRemoved { eq[PasswordAgeColOwnerRemoved.identifier()] = false } - stmt, scan := preparePasswordAgePolicyQuery(ctx, q.client) + stmt, scan := preparePasswordAgePolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -130,7 +129,7 @@ func (q *Queries) DefaultPasswordAgePolicy(ctx context.Context, shouldTriggerBul traceSpan.EndWithError(err) } - stmt, scan := preparePasswordAgePolicyQuery(ctx, q.client) + stmt, scan := preparePasswordAgePolicyQuery() query, args, err := stmt.Where(sq.Eq{ PasswordAgeColID.identifier(): authz.GetInstance(ctx).InstanceID(), }). @@ -147,7 +146,7 @@ func (q *Queries) DefaultPasswordAgePolicy(ctx context.Context, shouldTriggerBul return policy, err } -func preparePasswordAgePolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PasswordAgePolicy, error)) { +func preparePasswordAgePolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*PasswordAgePolicy, error)) { return sq.Select( PasswordAgeColID.identifier(), PasswordAgeColSequence.identifier(), @@ -159,7 +158,7 @@ func preparePasswordAgePolicyQuery(ctx context.Context, db prepareDatabase) (sq. PasswordAgeColIsDefault.identifier(), PasswordAgeColState.identifier(), ). - From(passwordAgeTable.identifier() + db.Timetravel(call.Took(ctx))). + From(passwordAgeTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PasswordAgePolicy, error) { policy := new(PasswordAgePolicy) diff --git a/internal/query/password_age_policy_test.go b/internal/query/password_age_policy_test.go index b140f82a06..f40acdb559 100644 --- a/internal/query/password_age_policy_test.go +++ b/internal/query/password_age_policy_test.go @@ -22,8 +22,7 @@ var ( ` projections.password_age_policies2.max_age_days,` + ` projections.password_age_policies2.is_default,` + ` projections.password_age_policies2.state` + - ` FROM projections.password_age_policies2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.password_age_policies2` preparePasswordAgePolicyCols = []string{ "id", "sequence", @@ -118,7 +117,7 @@ func Test_PasswordAgePolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/password_complexity_policy.go b/internal/query/password_complexity_policy.go index a895c98b75..fa0e5b2691 100644 --- a/internal/query/password_complexity_policy.go +++ b/internal/query/password_complexity_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -49,7 +48,7 @@ func (q *Queries) PasswordComplexityPolicyByOrg(ctx context.Context, shouldTrigg if !withOwnerRemoved { eq[PasswordComplexityColOwnerRemoved.identifier()] = false } - stmt, scan := preparePasswordComplexityPolicyQuery(ctx, q.client) + stmt, scan := preparePasswordComplexityPolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -82,7 +81,7 @@ func (q *Queries) DefaultPasswordComplexityPolicy(ctx context.Context, shouldTri traceSpan.EndWithError(err) } - stmt, scan := preparePasswordComplexityPolicyQuery(ctx, q.client) + stmt, scan := preparePasswordComplexityPolicyQuery() query, args, err := stmt.Where(sq.Eq{ PasswordComplexityColID.identifier(): authz.GetInstance(ctx).InstanceID(), PasswordComplexityColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -163,7 +162,7 @@ var ( } ) -func preparePasswordComplexityPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PasswordComplexityPolicy, error)) { +func preparePasswordComplexityPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*PasswordComplexityPolicy, error)) { return sq.Select( PasswordComplexityColID.identifier(), PasswordComplexityColSequence.identifier(), @@ -178,7 +177,7 @@ func preparePasswordComplexityPolicyQuery(ctx context.Context, db prepareDatabas PasswordComplexityColIsDefault.identifier(), PasswordComplexityColState.identifier(), ). - From(passwordComplexityTable.identifier() + db.Timetravel(call.Took(ctx))). + From(passwordComplexityTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PasswordComplexityPolicy, error) { policy := new(PasswordComplexityPolicy) diff --git a/internal/query/password_complexity_policy_test.go b/internal/query/password_complexity_policy_test.go index ac471f3994..e5738049dd 100644 --- a/internal/query/password_complexity_policy_test.go +++ b/internal/query/password_complexity_policy_test.go @@ -25,8 +25,7 @@ var ( ` projections.password_complexity_policies2.has_symbol,` + ` projections.password_complexity_policies2.is_default,` + ` projections.password_complexity_policies2.state` + - ` FROM projections.password_complexity_policies2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.password_complexity_policies2` preparePasswordComplexityPolicyCols = []string{ "id", "sequence", @@ -130,7 +129,7 @@ func Test_PasswordComplexityPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/permission.go b/internal/query/permission.go index 96d7db6c6a..19e3ed984e 100644 --- a/internal/query/permission.go +++ b/internal/query/permission.go @@ -2,34 +2,159 @@ package query import ( "context" - "fmt" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + domain_pkg "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" ) const ( - // eventstore.permitted_orgs(instanceid text, userid text, perm text) - wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?))" + // eventstore.permitted_orgs(req_instance_id text, auth_user_id text, system_user_perms JSONB, perm text, filter_org text) + joinPermittedOrgsFunction = `INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON ` + + // eventstore.permitted_projects(req_instance_id text, auth_user_id text, system_user_perms JSONB, perm text, filter_org text) + joinPermittedProjectsFunction = `INNER JOIN eventstore.permitted_projects(?, ?, ?, ?, ?) permissions ON ` ) -// wherePermittedOrgs sets a `WHERE` clause to the query that filters the orgs -// for which the authenticated user has the requested permission for. -// The user ID is taken from the context. -// -// The `orgIDColumn` specifies the table column to which this filter must be applied, -// and is typically the `resource_owner` column in ZITADEL. -// We use full identifiers in the query builder so this function should be -// called with something like `UserResourceOwnerCol.identifier()` for example. -func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, orgIDColumn, permission string) sq.SelectBuilder { - userID := authz.GetCtxData(ctx).UserID - logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "permission", permission, "user_id", userID).Debug("permitted orgs check used") - return query.Where( - fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn), - authz.GetInstance(ctx).InstanceID(), - userID, - permission, - ) +// permissionClauseBuilder is used to build the SQL clause for permission checks. +// Don't use it directly, use the [PermissionClause] function with proper options instead. +type permissionClauseBuilder struct { + orgIDColumn Column + instanceID string + userID string + systemPermissions []authz.SystemUserPermissions + permission string + + // optional fields + orgID *string + projectIDColumn *Column + connections []sq.Eq +} + +func (b *permissionClauseBuilder) appendConnection(column string, value any) { + b.connections = append(b.connections, sq.Eq{column: value}) +} + +// joinFunction picks the correct SQL function and return the required arguments for that function. +func (b *permissionClauseBuilder) joinFunction() (sql string, args []any) { + sql = joinPermittedOrgsFunction + if b.projectIDColumn != nil { + sql = joinPermittedProjectsFunction + } + return sql, []any{ + b.instanceID, + b.userID, + database.NewJSONArray(b.systemPermissions), + b.permission, + b.orgID, + } +} + +// joinConditions returns the conditions for the join, +// which are dynamic based on the provided options. +func (b *permissionClauseBuilder) joinConditions() sq.Or { + conditions := make(sq.Or, 2, len(b.connections)+3) + conditions[0] = sq.Expr("permissions.instance_permitted") + conditions[1] = sq.Expr(b.orgIDColumn.identifier() + " = ANY(permissions.org_ids)") + if b.projectIDColumn != nil { + conditions = append(conditions, + sq.Expr(b.projectIDColumn.identifier()+" = ANY(permissions.project_ids)"), + ) + } + for _, c := range b.connections { + conditions = append(conditions, c) + } + return conditions +} + +type PermissionOption func(b *permissionClauseBuilder) + +// OwnedRowsPermissionOption allows rows to be returned of which the current user is the owner. +// Even if the user does not have an explicit permission for the organization. +// For example an authenticated user can always see his own user account. +// This option may be provided multiple times to allow matching with multiple columns. +// See [ConnectionPermissionOption] for more details. +func OwnedRowsPermissionOption(userIDColumn Column) PermissionOption { + return func(b *permissionClauseBuilder) { + b.appendConnection(userIDColumn.identifier(), b.userID) + } +} + +// ConnectionPermissionOption allows returning of rows where the value is matched. +// Even if the user does not have an explicit permission for the resource. +// Multiple connections may be provided. +// Each connection is applied in a OR condition, so if previous permissions are not met, +// matching rows are still returned for a later match. +func ConnectionPermissionOption(column Column, value any) PermissionOption { + return func(b *permissionClauseBuilder) { + b.appendConnection(column.identifier(), value) + } +} + +// SingleOrgPermissionOption may be used to optimize the permitted orgs function by limiting the +// returned organizations, to the one used in the requested filters. +func SingleOrgPermissionOption(queries []SearchQuery) PermissionOption { + return func(b *permissionClauseBuilder) { + orgID, ok := findTextEqualsQuery(b.orgIDColumn, queries) + if ok { + b.orgID = &orgID + } + } +} + +// WithProjectsPermissionOption sets an additional filter against the project ID column, +// allowing for project specific permissions. +func WithProjectsPermissionOption(projectIDColumn Column) PermissionOption { + return func(b *permissionClauseBuilder) { + b.projectIDColumn = &projectIDColumn + } +} + +// PermissionClause builds a `INNER JOIN` clause which can be applied to a query builder. +// It filters returned rows the current authenticated user has the requested permission to. +// See permission_example_test.go for examples. +// +// Experimental: Work in progress. Currently only organization and project permissions are supported +// TODO: Add support for project grants. +func PermissionClause(ctx context.Context, orgIDCol Column, permission string, options ...PermissionOption) (string, []any) { + ctxData := authz.GetCtxData(ctx) + b := &permissionClauseBuilder{ + orgIDColumn: orgIDCol, + instanceID: authz.GetInstance(ctx).InstanceID(), + userID: ctxData.UserID, + systemPermissions: ctxData.SystemUserPermissions, + permission: permission, + } + for _, opt := range options { + opt(b) + } + logging.WithFields( + "org_id_column", b.orgIDColumn, + "instance_id", b.instanceID, + "user_id", b.userID, + "system_user_permissions", b.systemPermissions, + "permission", b.permission, + "org_id", b.orgID, + "project_id_column", b.projectIDColumn, + "connections", b.connections, + ).Debug("permitted orgs check used") + + sql, args := b.joinFunction() + conditions, conditionArgs, err := b.joinConditions().ToSql() + if err != nil { + // all cases are tested, no need to return an error. + // If an error does happen, it's a bug and not a user error. + panic(zerrors.ThrowInternal(err, "PERMISSION-OoS5o", "Errors.Internal")) + } + return sql + conditions, append(args, conditionArgs...) +} + +// PermissionV2 checks are enabled when the feature flag is set and the permission check function is not nil. +// When the permission check function is nil, it indicates a v1 API and no resource based permission check is needed. +func PermissionV2(ctx context.Context, cf domain_pkg.PermissionCheck) bool { + return authz.GetFeatures(ctx).PermissionCheckV2 && cf != nil } diff --git a/internal/query/permission_example_test.go b/internal/query/permission_example_test.go new file mode 100644 index 0000000000..6211ad0bb2 --- /dev/null +++ b/internal/query/permission_example_test.go @@ -0,0 +1,78 @@ +package query + +import ( + "context" + "fmt" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" +) + +// ExamplePermissionClause_org shows how to use the PermissionClause function to filter +// permitted records based on the resource owner and the user's instance or organization membership. +func ExamplePermissionClause_org() { + // These variables are typically set in the middleware of Zitadel. + // They do not influence the generation of the clause, just what + // the function does in Postgres. + ctx := authz.WithInstanceID(context.Background(), "instanceID") + ctx = authz.SetCtxData(ctx, authz.CtxData{ + UserID: "userID", + }) + + join, args := PermissionClause( + ctx, + UserResourceOwnerCol, // match the resource owner column + domain.PermissionUserRead, + SingleOrgPermissionOption([]SearchQuery{ + mustSearchQuery(NewUserDisplayNameSearchQuery("zitadel", TextContains)), + mustSearchQuery(NewUserResourceOwnerSearchQuery("orgID", TextEquals)), + }), // If the request had an orgID filter, it can be used to optimize the SQL function. + OwnedRowsPermissionOption(UserIDCol), // allow user to find themselves. + ) + + sql, _, _ := sq.Select("*"). + From(userTable.identifier()). + JoinClause(join, args...). + Where(sq.Eq{ + UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + }).ToSql() + fmt.Println(sql) + // Output: + // SELECT * FROM projections.users14 INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids) OR projections.users14.id = ?) WHERE projections.users14.instance_id = ? +} + +// ExamplePermissionClause_project shows how to use the PermissionClause function to filter +// permitted records based on the resource owner and the user's instance or organization membership. +// Additionally, it allows returning records based on the project ID and project membership. +func ExamplePermissionClause_project() { + // These variables are typically set in the middleware of Zitadel. + // They do not influence the generation of the clause, just what + // the function does in Postgres. + ctx := authz.WithInstanceID(context.Background(), "instanceID") + ctx = authz.SetCtxData(ctx, authz.CtxData{ + UserID: "userID", + }) + + join, args := PermissionClause( + ctx, + ProjectColumnResourceOwner, // match the resource owner column + "project.read", + WithProjectsPermissionOption(ProjectColumnID), + SingleOrgPermissionOption([]SearchQuery{ + mustSearchQuery(NewUserDisplayNameSearchQuery("zitadel", TextContains)), + mustSearchQuery(NewUserResourceOwnerSearchQuery("orgID", TextEquals)), + }), // If the request had an orgID filter, it can be used to optimize the SQL function. + ) + + sql, _, _ := sq.Select("*"). + From(projectsTable.identifier()). + JoinClause(join, args...). + Where(sq.Eq{ + ProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + }).ToSql() + fmt.Println(sql) + // Output: + // SELECT * FROM projections.projects4 INNER JOIN eventstore.permitted_projects(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.projects4.resource_owner = ANY(permissions.org_ids) OR projections.projects4.id = ANY(permissions.project_ids)) WHERE projections.projects4.instance_id = ? +} diff --git a/internal/query/permission_test.go b/internal/query/permission_test.go new file mode 100644 index 0000000000..24692a9406 --- /dev/null +++ b/internal/query/permission_test.go @@ -0,0 +1,243 @@ +package query + +import ( + "context" + "testing" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + domain_pkg "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/feature" +) + +func TestPermissionClause(t *testing.T) { + var permissions = []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"permission1", "permission2"}, + }, + { + MemberType: authz.MemberTypeIAM, + Permissions: []string{"permission2", "permission3"}, + }, + } + ctx := authz.WithInstanceID(context.Background(), "instanceID") + ctx = authz.SetCtxData(ctx, authz.CtxData{ + UserID: "userID", + SystemUserPermissions: permissions, + }) + + type args struct { + ctx context.Context + orgIDCol Column + permission string + options []PermissionOption + } + tests := []struct { + name string + args args + wantSql string + wantArgs []any + }{ + { + name: "org, no options", + args: args{ + ctx: ctx, + orgIDCol: UserResourceOwnerCol, + permission: "permission1", + }, + wantSql: "INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids))", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + (*string)(nil), + }, + }, + { + name: "org, owned rows option", + args: args{ + ctx: ctx, + orgIDCol: UserResourceOwnerCol, + permission: "permission1", + options: []PermissionOption{ + OwnedRowsPermissionOption(UserIDCol), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids) OR projections.users14.id = ?)", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + (*string)(nil), + "userID", + }, + }, + { + name: "org, connection rows option", + args: args{ + ctx: ctx, + orgIDCol: UserResourceOwnerCol, + permission: "permission1", + options: []PermissionOption{ + OwnedRowsPermissionOption(UserIDCol), + ConnectionPermissionOption(UserStateCol, "bar"), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids) OR projections.users14.id = ? OR projections.users14.state = ?)", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + (*string)(nil), + "userID", + "bar", + }, + }, + { + name: "org, with ID", + args: args{ + ctx: ctx, + orgIDCol: UserResourceOwnerCol, + permission: "permission1", + options: []PermissionOption{ + SingleOrgPermissionOption([]SearchQuery{ + mustSearchQuery(NewUserDisplayNameSearchQuery("zitadel", TextContains)), + mustSearchQuery(NewUserResourceOwnerSearchQuery("orgID", TextEquals)), + }), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids))", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + gu.Ptr("orgID"), + }, + }, + { + name: "project", + args: args{ + ctx: ctx, + orgIDCol: ProjectColumnResourceOwner, + permission: "permission1", + options: []PermissionOption{ + WithProjectsPermissionOption(ProjectColumnID), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_projects(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.projects4.resource_owner = ANY(permissions.org_ids) OR projections.projects4.id = ANY(permissions.project_ids))", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + (*string)(nil), + }, + }, + { + name: "project, single org", + args: args{ + ctx: ctx, + orgIDCol: ProjectColumnResourceOwner, + permission: "permission1", + options: []PermissionOption{ + WithProjectsPermissionOption(ProjectColumnID), + SingleOrgPermissionOption([]SearchQuery{ + mustSearchQuery(NewProjectResourceOwnerSearchQuery("orgID")), + }), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_projects(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.projects4.resource_owner = ANY(permissions.org_ids) OR projections.projects4.id = ANY(permissions.project_ids))", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + gu.Ptr("orgID"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotSql, gotArgs := PermissionClause(tt.args.ctx, tt.args.orgIDCol, tt.args.permission, tt.args.options...) + assert.Equal(t, tt.wantSql, gotSql) + assert.Equal(t, tt.wantArgs, gotArgs) + }) + } +} + +func mustSearchQuery(q SearchQuery, err error) SearchQuery { + if err != nil { + panic(err) + } + return q +} + +func TestPermissionV2(t *testing.T) { + type args struct { + ctx context.Context + cf domain_pkg.PermissionCheck + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "feature disabled, no permission check", + args: args{ + ctx: context.Background(), + cf: nil, + }, + want: false, + }, + { + name: "feature enabled, no permission check", + args: args{ + ctx: authz.WithFeatures(context.Background(), feature.Features{ + PermissionCheckV2: true, + }), + cf: nil, + }, + want: false, + }, + { + name: "feature enabled, with permission check", + args: args{ + ctx: authz.WithFeatures(context.Background(), feature.Features{ + PermissionCheckV2: true, + }), + cf: func(context.Context, string, string, string) error { + return nil + }, + }, + want: true, + }, + { + name: "feature disabled, with permission check", + args: args{ + ctx: authz.WithFeatures(context.Background(), feature.Features{ + PermissionCheckV2: false, + }), + cf: func(context.Context, string, string, string) error { + return nil + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := PermissionV2(tt.args.ctx, tt.args.cf) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/query/prepare_test.go b/internal/query/prepare_test.go index f8cf31cdef..e243426260 100644 --- a/internal/query/prepare_test.go +++ b/internal/query/prepare_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -32,7 +31,7 @@ var ( // func() (sq.SelectBuilder, func(*sql.Row) (*struct, error)) // expectedObject represents the return value of scan // sqlExpectation represents the query executed on the database -func assertPrepare(t *testing.T, prepareFunc, expectedObject interface{}, sqlExpectation sqlExpectation, isErr checkErr, prepareArgs ...reflect.Value) bool { +func assertPrepare(t *testing.T, prepareFunc, expectedObject any, sqlExpectation sqlExpectation, isErr checkErr, prepareArgs ...reflect.Value) bool { t.Helper() client, mock, err := sqlmock.New(sqlmock.ValueConverterOption(new(db_mock.TypeConverter))) @@ -243,9 +242,9 @@ func validateScan(scanType reflect.Type) error { return nil } -func execPrepare(prepare interface{}, args []reflect.Value) (builder sq.SelectBuilder, scan interface{}, err error) { +func execPrepare(prepare any, args []reflect.Value) (builder sq.SelectBuilder, scan interface{}, err error) { prepareVal := reflect.ValueOf(prepare) - if err := validatePrepare(prepareVal.Type()); err != nil { + if err := validatePrepare(prepareVal.Type(), len(args)); err != nil { return sq.SelectBuilder{}, nil, err } res := prepareVal.Call(args) @@ -253,12 +252,12 @@ func execPrepare(prepare interface{}, args []reflect.Value) (builder sq.SelectBu return res[0].Interface().(sq.SelectBuilder), res[1].Interface(), nil } -func validatePrepare(prepareType reflect.Type) error { +func validatePrepare(prepareType reflect.Type, numArgs int) error { if prepareType.Kind() != reflect.Func { return errors.New("prepare is not a function") } - if prepareType.NumIn() != 0 && prepareType.NumIn() != 2 { - return fmt.Errorf("prepare: invalid number of inputs: want: 0 or 2 got %d", prepareType.NumIn()) + if prepareType.NumIn() != numArgs { + return fmt.Errorf("prepare: invalid number of inputs: want: %d got %d", numArgs, prepareType.NumIn()) } if prepareType.NumOut() != 2 { return fmt.Errorf("prepare: invalid number of outputs: want: 2 got %d", prepareType.NumOut()) @@ -363,7 +362,7 @@ func TestValidatePrepare(t *testing.T) { }, { name: "correct", - t: reflect.TypeOf(func(context.Context, prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (interface{}, error)) { + t: reflect.TypeOf(func() (sq.SelectBuilder, func(*sql.Rows) (interface{}, error)) { log.Fatal("should not be executed") return sq.SelectBuilder{}, nil }), @@ -372,24 +371,10 @@ func TestValidatePrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validatePrepare(tt.t) + err := validatePrepare(tt.t, 0) if (err != nil) != tt.expectErr { t.Errorf("unexpected err: %v", err) } }) } } - -type prepareDB struct{} - -const asOfSystemTime = " AS OF SYSTEM TIME '-1 ms' " - -func (*prepareDB) Timetravel(time.Duration) string { return asOfSystemTime } - -var defaultPrepareArgs = []reflect.Value{reflect.ValueOf(context.Background()), reflect.ValueOf(new(prepareDB))} - -func (*prepareDB) DatabaseName() string { return "db" } - -func (*prepareDB) Username() string { return "user" } - -func (*prepareDB) Type() string { return "type" } diff --git a/internal/query/privacy_policy.go b/internal/query/privacy_policy.go index 59394e92b1..e26948f478 100644 --- a/internal/query/privacy_policy.go +++ b/internal/query/privacy_policy.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -122,7 +121,7 @@ func (q *Queries) PrivacyPolicyByOrg(ctx context.Context, shouldTriggerBulk bool if !withOwnerRemoved { eq[PrivacyColOwnerRemoved.identifier()] = false } - stmt, scan := preparePrivacyPolicyQuery(ctx, q.client) + stmt, scan := preparePrivacyPolicyQuery() query, args, err := stmt.Where( sq.And{ eq, @@ -154,7 +153,7 @@ func (q *Queries) DefaultPrivacyPolicy(ctx context.Context, shouldTriggerBulk bo traceSpan.EndWithError(err) } - stmt, scan := preparePrivacyPolicyQuery(ctx, q.client) + stmt, scan := preparePrivacyPolicyQuery() query, args, err := stmt.Where(sq.Eq{ PrivacyColID.identifier(): authz.GetInstance(ctx).InstanceID(), PrivacyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -172,7 +171,7 @@ func (q *Queries) DefaultPrivacyPolicy(ctx context.Context, shouldTriggerBulk bo return policy, err } -func preparePrivacyPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PrivacyPolicy, error)) { +func preparePrivacyPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*PrivacyPolicy, error)) { return sq.Select( PrivacyColID.identifier(), PrivacyColSequence.identifier(), @@ -189,7 +188,7 @@ func preparePrivacyPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele PrivacyColIsDefault.identifier(), PrivacyColState.identifier(), ). - From(privacyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(privacyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PrivacyPolicy, error) { policy := new(PrivacyPolicy) diff --git a/internal/query/privacy_policy_test.go b/internal/query/privacy_policy_test.go index ade541d0cc..1777ca1991 100644 --- a/internal/query/privacy_policy_test.go +++ b/internal/query/privacy_policy_test.go @@ -27,8 +27,7 @@ var ( ` projections.privacy_policies4.custom_link_text,` + ` projections.privacy_policies4.is_default,` + ` projections.privacy_policies4.state` + - ` FROM projections.privacy_policies4` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.privacy_policies4` preparePrivacyPolicyCols = []string{ "id", "sequence", @@ -138,7 +137,7 @@ func Test_PrivacyPolicyPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project.go b/internal/query/project.go index a92448f25d..7501047182 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -109,7 +108,7 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st traceSpan.EndWithError(err) } - stmt, scan := prepareProjectQuery(ctx, q.client) + stmt, scan := prepareProjectQuery() eq := sq.Eq{ ProjectColumnID.identifier(): id, ProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -130,7 +129,7 @@ func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQuer ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareProjectsQuery(ctx, q.client) + query, scan := prepareProjectsQuery() eq := sq.Eq{ProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -193,7 +192,7 @@ func (q *ProjectSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder return query } -func prepareProjectQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { +func prepareProjectQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), @@ -206,7 +205,7 @@ func prepareProjectQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil ProjectColumnProjectRoleCheck.identifier(), ProjectColumnHasProjectCheck.identifier(), ProjectColumnPrivateLabelingSetting.identifier()). - From(projectsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(projectsTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Project, error) { p := new(Project) @@ -233,7 +232,7 @@ func prepareProjectQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareProjectsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Projects, error)) { +func prepareProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Projects, error)) { return sq.Select( ProjectColumnID.identifier(), ProjectColumnCreationDate.identifier(), @@ -247,7 +246,7 @@ func prepareProjectsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui ProjectColumnHasProjectCheck.identifier(), ProjectColumnPrivateLabelingSetting.identifier(), countColumn.identifier()). - From(projectsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(projectsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Projects, error) { projects := make([]*Project, 0) diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index 1bc68e984a..b971593c77 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -116,7 +115,7 @@ func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool, traceSpan.EndWithError(err) } - stmt, scan := prepareProjectGrantQuery(ctx, q.client) + stmt, scan := prepareProjectGrantQuery() eq := sq.Eq{ ProjectGrantColumnGrantID.identifier(): id, ProjectGrantColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -137,7 +136,7 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareProjectGrantQuery(ctx, q.client) + stmt, scan := prepareProjectGrantQuery() eq := sq.Eq{ ProjectGrantColumnGrantID.identifier(): id, ProjectGrantColumnGrantedOrgID.identifier(): grantedOrg, @@ -159,7 +158,7 @@ func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrant ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareProjectGrantsQuery(ctx, q.client) + query, scan := prepareProjectGrantsQuery() eq := sq.Eq{ ProjectGrantColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -264,7 +263,7 @@ func (q *ProjectGrantSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBui return query } -func prepareProjectGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*ProjectGrant, error)) { +func prepareProjectGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*ProjectGrant, error)) { resourceOwnerOrgTable := orgsTable.setAlias(ProjectGrantResourceOwnerTableAlias) resourceOwnerIDColumn := OrgColumnID.setTable(resourceOwnerOrgTable) grantedOrgTable := orgsTable.setAlias(ProjectGrantGrantedOrgTableAlias) @@ -286,7 +285,7 @@ func prepareProjectGrantQuery(ctx context.Context, db prepareDatabase) (sq.Selec PlaceholderFormat(sq.Dollar). LeftJoin(join(ProjectColumnID, ProjectGrantColumnProjectID)). LeftJoin(join(resourceOwnerIDColumn, ProjectGrantColumnResourceOwner)). - LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID) + db.Timetravel(call.Took(ctx))), + LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID)), func(row *sql.Row) (*ProjectGrant, error) { grant := new(ProjectGrant) var ( @@ -323,7 +322,7 @@ func prepareProjectGrantQuery(ctx context.Context, db prepareDatabase) (sq.Selec } } -func prepareProjectGrantsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*ProjectGrants, error)) { +func prepareProjectGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*ProjectGrants, error)) { resourceOwnerOrgTable := orgsTable.setAlias(ProjectGrantResourceOwnerTableAlias) resourceOwnerIDColumn := OrgColumnID.setTable(resourceOwnerOrgTable) grantedOrgTable := orgsTable.setAlias(ProjectGrantGrantedOrgTableAlias) @@ -346,7 +345,7 @@ func prepareProjectGrantsQuery(ctx context.Context, db prepareDatabase) (sq.Sele PlaceholderFormat(sq.Dollar). LeftJoin(join(ProjectColumnID, ProjectGrantColumnProjectID)). LeftJoin(join(resourceOwnerIDColumn, ProjectGrantColumnResourceOwner)). - LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID) + db.Timetravel(call.Took(ctx))), + LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID)), func(rows *sql.Rows) (*ProjectGrants, error) { projects := make([]*ProjectGrant, 0) var ( diff --git a/internal/query/project_grant_member.go b/internal/query/project_grant_member.go index 0820ada826..a9cc49c498 100644 --- a/internal/query/project_grant_member.go +++ b/internal/query/project_grant_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" @@ -82,7 +81,7 @@ func (q *ProjectGrantMembersQuery) toQuery(query sq.SelectBuilder) sq.SelectBuil } func (q *Queries) ProjectGrantMembers(ctx context.Context, queries *ProjectGrantMembersQuery) (members *Members, err error) { - query, scan := prepareProjectGrantMembersQuery(ctx, q.client) + query, scan := prepareProjectGrantMembersQuery() eq := sq.Eq{ProjectGrantMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -106,7 +105,7 @@ func (q *Queries) ProjectGrantMembers(ctx context.Context, queries *ProjectGrant return members, err } -func prepareProjectGrantMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareProjectGrantMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( ProjectGrantMemberCreationDate.identifier(), ProjectGrantMemberChangeDate.identifier(), @@ -129,7 +128,7 @@ func prepareProjectGrantMembersQuery(ctx context.Context, db prepareDatabase) (s LeftJoin(join(MachineUserIDCol, ProjectGrantMemberUserID)). LeftJoin(join(UserIDCol, ProjectGrantMemberUserID)). LeftJoin(join(LoginNameUserIDCol, ProjectGrantMemberUserID)). - LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index 72eaf76d6e..23d1258b7c 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -46,7 +46,6 @@ var ( "LEFT JOIN projections.project_grants4 " + "ON members.grant_id = projections.project_grants4.grant_id " + "AND members.instance_id = projections.project_grants4.instance_id " + - `AS OF SYSTEM TIME '-1 ms' ` + "WHERE projections.login_names3.is_primary = $1") projectGrantMembersColumns = []string{ "creation_date", @@ -302,7 +301,7 @@ func Test_ProjectGrantMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project_grant_test.go b/internal/query/project_grant_test.go index 6d2131dfc4..2801e0b23e 100644 --- a/internal/query/project_grant_test.go +++ b/internal/query/project_grant_test.go @@ -30,8 +30,7 @@ var ( ` FROM projections.project_grants4 ` + ` LEFT JOIN projections.projects4 ON projections.project_grants4.project_id = projections.projects4.id AND projections.project_grants4.instance_id = projections.projects4.instance_id ` + ` LEFT JOIN projections.orgs1 AS r ON projections.project_grants4.resource_owner = r.id AND projections.project_grants4.instance_id = r.instance_id` + - ` LEFT JOIN projections.orgs1 AS o ON projections.project_grants4.granted_org_id = o.id AND projections.project_grants4.instance_id = o.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.orgs1 AS o ON projections.project_grants4.granted_org_id = o.id AND projections.project_grants4.instance_id = o.instance_id` projectGrantsCols = []string{ "project_id", "grant_id", @@ -62,8 +61,7 @@ var ( ` FROM projections.project_grants4 ` + ` LEFT JOIN projections.projects4 ON projections.project_grants4.project_id = projections.projects4.id AND projections.project_grants4.instance_id = projections.projects4.instance_id ` + ` LEFT JOIN projections.orgs1 AS r ON projections.project_grants4.resource_owner = r.id AND projections.project_grants4.instance_id = r.instance_id` + - ` LEFT JOIN projections.orgs1 AS o ON projections.project_grants4.granted_org_id = o.id AND projections.project_grants4.instance_id = o.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.orgs1 AS o ON projections.project_grants4.granted_org_id = o.id AND projections.project_grants4.instance_id = o.instance_id` projectGrantCols = []string{ "project_id", "grant_id", @@ -573,7 +571,7 @@ func Test_ProjectGrantPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project_member.go b/internal/query/project_member.go index 347eac12b9..1b66b45ccc 100644 --- a/internal/query/project_member.go +++ b/internal/query/project_member.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -73,7 +72,7 @@ func (q *Queries) ProjectMembers(ctx context.Context, queries *ProjectMembersQue ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareProjectMembersQuery(ctx, q.client) + query, scan := prepareProjectMembersQuery() eq := sq.Eq{ProjectMemberInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -97,7 +96,7 @@ func (q *Queries) ProjectMembers(ctx context.Context, queries *ProjectMembersQue return members, err } -func prepareProjectMembersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { +func prepareProjectMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, error)) { return sq.Select( ProjectMemberCreationDate.identifier(), ProjectMemberChangeDate.identifier(), @@ -119,7 +118,7 @@ func prepareProjectMembersQuery(ctx context.Context, db prepareDatabase) (sq.Sel LeftJoin(join(HumanUserIDCol, ProjectMemberUserID)). LeftJoin(join(MachineUserIDCol, ProjectMemberUserID)). LeftJoin(join(UserIDCol, ProjectMemberUserID)). - LeftJoin(join(LoginNameUserIDCol, ProjectMemberUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, ProjectMemberUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/project_member_test.go b/internal/query/project_member_test.go index 24548dadfb..6e552eb2ec 100644 --- a/internal/query/project_member_test.go +++ b/internal/query/project_member_test.go @@ -43,7 +43,6 @@ var ( "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + - `AS OF SYSTEM TIME '-1 ms' ` + "WHERE projections.login_names3.is_primary = $1") projectMembersColumns = []string{ "creation_date", @@ -299,7 +298,7 @@ func Test_ProjectMemberPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project_role.go b/internal/query/project_role.go index 76e113da65..ab4f40ca38 100644 --- a/internal/query/project_role.go +++ b/internal/query/project_role.go @@ -9,7 +9,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -94,7 +93,7 @@ func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool eq := sq.Eq{ProjectRoleColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} - query, scan := prepareProjectRolesQuery(ctx, q.client) + query, scan := prepareProjectRolesQuery() stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-3N9ff", "Errors.Query.InvalidRequest") @@ -126,7 +125,7 @@ func (q *Queries) SearchGrantedProjectRoles(ctx context.Context, grantID, grante eq := sq.Eq{ProjectRoleColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} - query, scan := prepareProjectRolesQuery(ctx, q.client) + query, scan := prepareProjectRolesQuery() stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-3N9ff", "Errors.Query.InvalidRequest") @@ -207,7 +206,7 @@ func (q *ProjectRoleSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuil return query } -func prepareProjectRolesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*ProjectRoles, error)) { +func prepareProjectRolesQuery() (sq.SelectBuilder, func(*sql.Rows) (*ProjectRoles, error)) { return sq.Select( ProjectRoleColumnProjectID.identifier(), ProjectRoleColumnCreationDate.identifier(), @@ -218,7 +217,7 @@ func prepareProjectRolesQuery(ctx context.Context, db prepareDatabase) (sq.Selec ProjectRoleColumnDisplayName.identifier(), ProjectRoleColumnGroupName.identifier(), countColumn.identifier()). - From(projectRolesTable.identifier() + db.Timetravel(call.Took(ctx))). + From(projectRolesTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*ProjectRoles, error) { projects := make([]*ProjectRole, 0) diff --git a/internal/query/project_role_test.go b/internal/query/project_role_test.go index 516a4df169..468aafaa19 100644 --- a/internal/query/project_role_test.go +++ b/internal/query/project_role_test.go @@ -19,8 +19,7 @@ var ( ` projections.project_roles4.display_name,` + ` projections.project_roles4.group_name,` + ` COUNT(*) OVER ()` + - ` FROM projections.project_roles4` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.project_roles4` prepareProjectRolesCols = []string{ "project_id", "creation_date", @@ -175,7 +174,7 @@ func Test_ProjectRolePrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/project_test.go b/internal/query/project_test.go index a621c27f42..1eafcb69a8 100644 --- a/internal/query/project_test.go +++ b/internal/query/project_test.go @@ -39,8 +39,7 @@ var ( ` projections.projects4.has_project_check,` + ` projections.projects4.private_labeling_setting,` + ` COUNT(*) OVER ()` + - ` FROM projections.projects4` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.projects4` prepareProjectsCols = []string{ "id", "creation_date", @@ -67,8 +66,7 @@ var ( ` projections.projects4.project_role_check,` + ` projections.projects4.has_project_check,` + ` projections.projects4.private_labeling_setting` + - ` FROM projections.projects4` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.projects4` prepareProjectCols = []string{ "id", "creation_date", @@ -314,7 +312,7 @@ func Test_ProjectPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/projection/app.go b/internal/query/projection/app.go index 14053cc8dc..c50bf03f40 100644 --- a/internal/query/projection/app.go +++ b/internal/query/projection/app.go @@ -62,12 +62,14 @@ const ( AppOIDCConfigColumnLoginVersion = "login_version" AppOIDCConfigColumnLoginBaseURI = "login_base_uri" - appSAMLTableSuffix = "saml_configs" - AppSAMLConfigColumnAppID = "app_id" - AppSAMLConfigColumnInstanceID = "instance_id" - AppSAMLConfigColumnEntityID = "entity_id" - AppSAMLConfigColumnMetadata = "metadata" - AppSAMLConfigColumnMetadataURL = "metadata_url" + appSAMLTableSuffix = "saml_configs" + AppSAMLConfigColumnAppID = "app_id" + AppSAMLConfigColumnInstanceID = "instance_id" + AppSAMLConfigColumnEntityID = "entity_id" + AppSAMLConfigColumnMetadata = "metadata" + AppSAMLConfigColumnMetadataURL = "metadata_url" + AppSAMLConfigColumnLoginVersion = "login_version" + AppSAMLConfigColumnLoginBaseURI = "login_base_uri" ) type appProjection struct{} @@ -143,6 +145,8 @@ func (*appProjection) Init() *old_handler.Check { handler.NewColumn(AppSAMLConfigColumnEntityID, handler.ColumnTypeText), handler.NewColumn(AppSAMLConfigColumnMetadata, handler.ColumnTypeBytes), handler.NewColumn(AppSAMLConfigColumnMetadataURL, handler.ColumnTypeText), + handler.NewColumn(AppSAMLConfigColumnLoginVersion, handler.ColumnTypeEnum, handler.Nullable()), + handler.NewColumn(AppSAMLConfigColumnLoginBaseURI, handler.ColumnTypeText, handler.Nullable()), }, handler.NewPrimaryKey(AppSAMLConfigColumnInstanceID, AppSAMLConfigColumnAppID), appSAMLTableSuffix, @@ -703,6 +707,8 @@ func (p *appProjection) reduceSAMLConfigAdded(event eventstore.Event) (*handler. handler.NewCol(AppSAMLConfigColumnEntityID, e.EntityID), handler.NewCol(AppSAMLConfigColumnMetadata, e.Metadata), handler.NewCol(AppSAMLConfigColumnMetadataURL, e.MetadataURL), + handler.NewCol(AppSAMLConfigColumnLoginVersion, e.LoginVersion), + handler.NewCol(AppSAMLConfigColumnLoginBaseURI, e.LoginBaseURI), }, handler.WithTableSuffix(appSAMLTableSuffix), ), @@ -735,6 +741,12 @@ func (p *appProjection) reduceSAMLConfigChanged(event eventstore.Event) (*handle if e.EntityID != "" { cols = append(cols, handler.NewCol(AppSAMLConfigColumnEntityID, e.EntityID)) } + if e.LoginVersion != nil { + cols = append(cols, handler.NewCol(AppSAMLConfigColumnLoginVersion, *e.LoginVersion)) + } + if e.LoginBaseURI != nil { + cols = append(cols, handler.NewCol(AppSAMLConfigColumnLoginBaseURI, *e.LoginBaseURI)) + } if len(cols) == 0 { return handler.NewNoOpStatement(e), nil diff --git a/internal/query/projection/eventstore_field.go b/internal/query/projection/eventstore_field.go index 5dbdad717a..73e3ac2c82 100644 --- a/internal/query/projection/eventstore_field.go +++ b/internal/query/projection/eventstore_field.go @@ -5,6 +5,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/permission" "github.com/zitadel/zitadel/internal/repository/project" ) @@ -13,6 +14,7 @@ const ( fieldsOrgDomainVerified = "org_domain_verified_fields" fieldsInstanceDomain = "instance_domain_fields" fieldsMemberships = "membership_fields" + fieldsPermission = "permission_fields" ) func newFillProjectGrantFields(config handler.Config) *handler.FieldHandler { @@ -83,3 +85,16 @@ func newFillMembershipFields(config handler.Config) *handler.FieldHandler { }, ) } + +func newFillPermissionFields(config handler.Config) *handler.FieldHandler { + return handler.NewFieldHandler( + &config, + permission.PermissionSearchField, + map[eventstore.AggregateType][]eventstore.EventType{ + permission.AggregateType: { + permission.AddedType, + permission.RemovedType, + }, + }, + ) +} diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index a9d38cfebb..55c74b851c 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -70,6 +70,7 @@ const ( OAuthUserEndpointCol = "user_endpoint" OAuthScopesCol = "scopes" OAuthIDAttributeCol = "id_attribute" + OAuthUsePKCECol = "use_pkce" OIDCIDCol = "idp_id" OIDCInstanceIDCol = "instance_id" @@ -78,6 +79,7 @@ const ( OIDCClientSecretCol = "client_secret" OIDCScopesCol = "scopes" OIDCIDTokenMappingCol = "id_token_mapping" + OIDCUsePKCECol = "use_pkce" JWTIDCol = "idp_id" JWTInstanceIDCol = "instance_id" @@ -139,6 +141,7 @@ const ( LDAPUserObjectClassesCol = "user_object_classes" LDAPUserFiltersCol = "user_filters" LDAPTimeoutCol = "timeout" + LDAPRootCACol = "root_ca" LDAPIDAttributeCol = "id_attribute" LDAPFirstNameAttributeCol = "first_name_attribute" LDAPLastNameAttributeCol = "last_name_attribute" @@ -216,6 +219,7 @@ func (*idpTemplateProjection) Init() *old_handler.Check { handler.NewColumn(OAuthUserEndpointCol, handler.ColumnTypeText), handler.NewColumn(OAuthScopesCol, handler.ColumnTypeTextArray, handler.Nullable()), handler.NewColumn(OAuthIDAttributeCol, handler.ColumnTypeText), + handler.NewColumn(OAuthUsePKCECol, handler.ColumnTypeBool, handler.Default(false)), }, handler.NewPrimaryKey(OAuthInstanceIDCol, OAuthIDCol), IDPTemplateOAuthSuffix, @@ -229,6 +233,7 @@ func (*idpTemplateProjection) Init() *old_handler.Check { handler.NewColumn(OIDCClientSecretCol, handler.ColumnTypeJSONB), handler.NewColumn(OIDCScopesCol, handler.ColumnTypeTextArray, handler.Nullable()), handler.NewColumn(OIDCIDTokenMappingCol, handler.ColumnTypeBool, handler.Default(false)), + handler.NewColumn(OIDCUsePKCECol, handler.ColumnTypeBool, handler.Default(false)), }, handler.NewPrimaryKey(OIDCInstanceIDCol, OIDCIDCol), IDPTemplateOIDCSuffix, @@ -330,6 +335,7 @@ func (*idpTemplateProjection) Init() *old_handler.Check { handler.NewColumn(LDAPUserObjectClassesCol, handler.ColumnTypeTextArray), handler.NewColumn(LDAPUserFiltersCol, handler.ColumnTypeTextArray), handler.NewColumn(LDAPTimeoutCol, handler.ColumnTypeInt64), + handler.NewColumn(LDAPRootCACol, handler.ColumnTypeBytes, handler.Nullable()), handler.NewColumn(LDAPIDAttributeCol, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(LDAPFirstNameAttributeCol, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(LDAPLastNameAttributeCol, handler.ColumnTypeText, handler.Nullable()), @@ -720,6 +726,7 @@ func (p *idpTemplateProjection) reduceOAuthIDPAdded(event eventstore.Event) (*ha handler.NewCol(OAuthUserEndpointCol, idpEvent.UserEndpoint), handler.NewCol(OAuthScopesCol, database.TextArray[string](idpEvent.Scopes)), handler.NewCol(OAuthIDAttributeCol, idpEvent.IDAttribute), + handler.NewCol(OAuthUsePKCECol, idpEvent.UsePKCE), }, handler.WithTableSuffix(IDPTemplateOAuthSuffix), ), @@ -811,6 +818,7 @@ func (p *idpTemplateProjection) reduceOIDCIDPAdded(event eventstore.Event) (*han handler.NewCol(OIDCClientSecretCol, idpEvent.ClientSecret), handler.NewCol(OIDCScopesCol, database.TextArray[string](idpEvent.Scopes)), handler.NewCol(OIDCIDTokenMappingCol, idpEvent.IsIDTokenMapping), + handler.NewCol(OIDCUsePKCECol, idpEvent.UsePKCE), }, handler.WithTableSuffix(IDPTemplateOIDCSuffix), ), @@ -1152,6 +1160,7 @@ func (p *idpTemplateProjection) reduceOldOIDCConfigAdded(event eventstore.Event) handler.NewCol(OIDCClientSecretCol, idpEvent.ClientSecret), handler.NewCol(OIDCScopesCol, database.TextArray[string](idpEvent.Scopes)), handler.NewCol(OIDCIDTokenMappingCol, true), + handler.NewCol(OIDCUsePKCECol, false), }, handler.WithTableSuffix(IDPTemplateOIDCSuffix), ), @@ -1896,6 +1905,7 @@ func (p *idpTemplateProjection) reduceLDAPIDPAdded(event eventstore.Event) (*han handler.NewCol(LDAPUserObjectClassesCol, database.TextArray[string](idpEvent.UserObjectClasses)), handler.NewCol(LDAPUserFiltersCol, database.TextArray[string](idpEvent.UserFilters)), handler.NewCol(LDAPTimeoutCol, idpEvent.Timeout), + handler.NewCol(LDAPRootCACol, idpEvent.RootCA), handler.NewCol(LDAPIDAttributeCol, idpEvent.IDAttribute), handler.NewCol(LDAPFirstNameAttributeCol, idpEvent.FirstNameAttribute), handler.NewCol(LDAPLastNameAttributeCol, idpEvent.LastNameAttribute), @@ -2250,6 +2260,9 @@ func reduceOAuthIDPChangedColumns(idpEvent idp.OAuthIDPChangedEvent) []handler.C if idpEvent.IDAttribute != nil { oauthCols = append(oauthCols, handler.NewCol(OAuthIDAttributeCol, *idpEvent.IDAttribute)) } + if idpEvent.UsePKCE != nil { + oauthCols = append(oauthCols, handler.NewCol(OAuthUsePKCECol, *idpEvent.UsePKCE)) + } return oauthCols } @@ -2270,6 +2283,9 @@ func reduceOIDCIDPChangedColumns(idpEvent idp.OIDCIDPChangedEvent) []handler.Col if idpEvent.IsIDTokenMapping != nil { oidcCols = append(oidcCols, handler.NewCol(OIDCIDTokenMappingCol, *idpEvent.IsIDTokenMapping)) } + if idpEvent.UsePKCE != nil { + oidcCols = append(oidcCols, handler.NewCol(OIDCUsePKCECol, *idpEvent.UsePKCE)) + } return oidcCols } @@ -2421,6 +2437,9 @@ func reduceLDAPIDPChangedColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Col if idpEvent.Timeout != nil { ldapCols = append(ldapCols, handler.NewCol(LDAPTimeoutCol, *idpEvent.Timeout)) } + if idpEvent.RootCA != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPRootCACol, idpEvent.RootCA)) + } if idpEvent.IDAttribute != nil { ldapCols = append(ldapCols, handler.NewCol(LDAPIDAttributeCol, *idpEvent.IDAttribute)) } diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index 6f096a94ce..cebf3f8791 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -192,6 +192,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { "userEndpoint": "user", "scopes": ["profile"], "idAttribute": "id-attribute", + "usePKCE": false, "isCreationAllowed": true, "isLinkingAllowed": true, "isAutoCreation": true, @@ -227,7 +228,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_oauth2 (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.idp_templates6_oauth2 (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute, use_pkce) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -238,6 +239,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { "user", database.TextArray[string]{"profile"}, "id-attribute", + false, }, }, }, @@ -265,6 +267,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { "userEndpoint": "user", "scopes": ["profile"], "idAttribute": "id-attribute", + "usePKCE": true, "isCreationAllowed": true, "isLinkingAllowed": true, "isAutoCreation": true, @@ -300,7 +303,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_oauth2 (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.idp_templates6_oauth2 (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute, use_pkce) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -311,6 +314,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { "user", database.TextArray[string]{"profile"}, "id-attribute", + true, }, }, }, @@ -380,6 +384,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { "userEndpoint": "user", "scopes": ["profile"], "idAttribute": "id-attribute", + "usePKCE": true, "isCreationAllowed": true, "isLinkingAllowed": true, "isAutoCreation": true, @@ -410,7 +415,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.idp_templates6_oauth2 SET (client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute) = ($1, $2, $3, $4, $5, $6, $7) WHERE (idp_id = $8) AND (instance_id = $9)", + expectedStmt: "UPDATE projections.idp_templates6_oauth2 SET (client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute, use_pkce) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (idp_id = $9) AND (instance_id = $10)", expectedArgs: []interface{}{ "client_id", anyArg{}, @@ -419,6 +424,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) { "user", database.TextArray[string]{"profile"}, "id-attribute", + true, "idp-id", "instance-id", }, @@ -2117,6 +2123,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { "userObjectClasses": ["object"], "userFilters": ["filter"], "timeout": 30000000000, + "rootCA": `+stringToJSONByte("certificate")+`, "idAttribute": "id", "firstNameAttribute": "first", "lastNameAttribute": "last", @@ -2165,7 +2172,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_ldap2 (idp_id, instance_id, servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)", + expectedStmt: "INSERT INTO projections.idp_templates6_ldap2 (idp_id, instance_id, servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, root_ca, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -2178,6 +2185,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { database.TextArray[string]{"object"}, database.TextArray[string]{"filter"}, time.Duration(30000000000), + []byte("certificate"), "id", "first", "last", @@ -2220,6 +2228,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { "userObjectClasses": ["object"], "userFilters": ["filter"], "timeout": 30000000000, + "rootCA": `+stringToJSONByte("certificate")+`, "idAttribute": "id", "firstNameAttribute": "first", "lastNameAttribute": "last", @@ -2268,7 +2277,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_ldap2 (idp_id, instance_id, servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)", + expectedStmt: "INSERT INTO projections.idp_templates6_ldap2 (idp_id, instance_id, servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, root_ca, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -2281,6 +2290,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { database.TextArray[string]{"object"}, database.TextArray[string]{"filter"}, time.Duration(30000000000), + []byte("certificate"), "id", "first", "last", @@ -2365,6 +2375,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { "userObjectClasses": ["object"], "userFilters": ["filter"], "timeout": 30000000000, + "rootCA": `+stringToJSONByte("certificate")+`, "idAttribute": "id", "firstNameAttribute": "first", "lastNameAttribute": "last", @@ -2408,7 +2419,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.idp_templates6_ldap2 SET (servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) WHERE (idp_id = $23) AND (instance_id = $24)", + expectedStmt: "UPDATE projections.idp_templates6_ldap2 SET (servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, root_ca, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) WHERE (idp_id = $24) AND (instance_id = $25)", expectedArgs: []interface{}{ database.TextArray[string]{"server"}, false, @@ -2419,6 +2430,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { database.TextArray[string]{"object"}, database.TextArray[string]{"filter"}, time.Duration(30000000000), + []byte("certificate"), "id", "first", "last", @@ -3075,6 +3087,7 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { }, "scopes": ["profile"], "idTokenMapping": true, + "usePKCE": true, "isCreationAllowed": true, "isLinkingAllowed": true, "isAutoCreation": true, @@ -3110,7 +3123,7 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_oidc (idp_id, instance_id, issuer, client_id, client_secret, scopes, id_token_mapping) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedStmt: "INSERT INTO projections.idp_templates6_oidc (idp_id, instance_id, issuer, client_id, client_secret, scopes, id_token_mapping, use_pkce) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -3119,6 +3132,7 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { anyArg{}, database.TextArray[string]{"profile"}, true, + true, }, }, }, @@ -3143,6 +3157,7 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { }, "scopes": ["profile"], "idTokenMapping": true, + "usePKCE": true, "isCreationAllowed": true, "isLinkingAllowed": true, "isAutoCreation": true, @@ -3178,7 +3193,7 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_oidc (idp_id, instance_id, issuer, client_id, client_secret, scopes, id_token_mapping) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedStmt: "INSERT INTO projections.idp_templates6_oidc (idp_id, instance_id, issuer, client_id, client_secret, scopes, id_token_mapping, use_pkce) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -3187,6 +3202,7 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { anyArg{}, database.TextArray[string]{"profile"}, true, + true, }, }, }, @@ -3254,6 +3270,7 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { }, "scopes": ["profile"], "idTokenMapping": true, + "usePKCE": true, "isCreationAllowed": true, "isLinkingAllowed": true, "isAutoCreation": true, @@ -3284,13 +3301,14 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.idp_templates6_oidc SET (client_id, client_secret, issuer, scopes, id_token_mapping) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)", + expectedStmt: "UPDATE projections.idp_templates6_oidc SET (client_id, client_secret, issuer, scopes, id_token_mapping, use_pkce) = ($1, $2, $3, $4, $5, $6) WHERE (idp_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "client_id", anyArg{}, "issuer", database.TextArray[string]{"profile"}, true, + true, "idp-id", "instance-id", }, @@ -3804,7 +3822,7 @@ func TestIDPTemplateProjection_reducesOldConfig(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_oidc (idp_id, instance_id, issuer, client_id, client_secret, scopes, id_token_mapping) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedStmt: "INSERT INTO projections.idp_templates6_oidc (idp_id, instance_id, issuer, client_id, client_secret, scopes, id_token_mapping, use_pkce) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", expectedArgs: []interface{}{ "idp-config-id", "instance-id", @@ -3813,6 +3831,7 @@ func TestIDPTemplateProjection_reducesOldConfig(t *testing.T) { anyArg{}, database.TextArray[string]{"profile"}, true, + false, }, }, }, @@ -3858,7 +3877,7 @@ func TestIDPTemplateProjection_reducesOldConfig(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_oidc (idp_id, instance_id, issuer, client_id, client_secret, scopes, id_token_mapping) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedStmt: "INSERT INTO projections.idp_templates6_oidc (idp_id, instance_id, issuer, client_id, client_secret, scopes, id_token_mapping, use_pkce) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", expectedArgs: []interface{}{ "idp-config-id", "instance-id", @@ -3867,6 +3886,7 @@ func TestIDPTemplateProjection_reducesOldConfig(t *testing.T) { anyArg{}, database.TextArray[string]{"profile"}, true, + false, }, }, }, diff --git a/internal/query/projection/instance_features.go b/internal/query/projection/instance_features.go index 2cd846bf2e..34100a0d66 100644 --- a/internal/query/projection/instance_features.go +++ b/internal/query/projection/instance_features.go @@ -80,10 +80,6 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceTokenExchangeEventType, Reduce: reduceInstanceSetFeature[bool], }, - { - Event: feature_v2.InstanceActionsEventType, - Reduce: reduceInstanceSetFeature[bool], - }, { Event: feature_v2.InstanceImprovedPerformanceEventType, Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType], @@ -116,6 +112,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstancePermissionCheckV2, Reduce: reduceInstanceSetFeature[bool], }, + { + Event: feature_v2.InstanceConsoleUseV2UserApi, + Reduce: reduceInstanceSetFeature[bool], + }, { Event: instance.InstanceRemovedEventType, Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol), diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index d6647d0961..f4e3bbe0d4 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -86,6 +86,7 @@ var ( OrgDomainVerifiedFields *handler.FieldHandler InstanceDomainFields *handler.FieldHandler MembershipFields *handler.FieldHandler + PermissionFields *handler.FieldHandler ) type projection interface { @@ -97,6 +98,7 @@ type projection interface { var ( projections []projection + fields []*handler.FieldHandler ) func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, config Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm, systemUsers map[string]*internal_authz.SystemAPIUser) error { @@ -176,8 +178,11 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified])) InstanceDomainFields = newFillInstanceDomainFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsInstanceDomain])) MembershipFields = newFillMembershipFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsMemberships])) + PermissionFields = newFillPermissionFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsPermission])) + // Don't forget to add the new field handler to [ProjectInstanceFields] newProjectionsList() + newFieldsList() return nil } @@ -210,6 +215,16 @@ func ProjectInstance(ctx context.Context) error { return nil } +func ProjectInstanceFields(ctx context.Context) error { + for _, fieldProjection := range fields { + err := fieldProjection.Trigger(ctx) + if err != nil { + return err + } + } + return nil +} + func ApplyCustomConfig(customConfig CustomConfig) handler.Config { return applyCustomConfig(projectionConfig, customConfig) } @@ -234,6 +249,16 @@ func applyCustomConfig(config handler.Config, customConfig CustomConfig) handler return config } +func newFieldsList() { + fields = []*handler.FieldHandler{ + ProjectGrantFields, + OrgDomainVerifiedFields, + InstanceDomainFields, + MembershipFields, + PermissionFields, + } +} + // we know this is ugly, but we need to have a singleton slice of all projections // and are only able to initialize it after all projections are created // as setup and start currently create them individually, we make sure we get the right one diff --git a/internal/query/projection/system_features.go b/internal/query/projection/system_features.go index f6f0a36d56..de54054e78 100644 --- a/internal/query/projection/system_features.go +++ b/internal/query/projection/system_features.go @@ -72,10 +72,6 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.SystemTokenExchangeEventType, Reduce: reduceSystemSetFeature[bool], }, - { - Event: feature_v2.SystemActionsEventType, - Reduce: reduceSystemSetFeature[bool], - }, { Event: feature_v2.SystemImprovedPerformanceEventType, Reduce: reduceSystemSetFeature[[]feature.ImprovedPerformanceType], diff --git a/internal/query/query.go b/internal/query/query.go index 0a90e9e4f9..e2e7f58ffc 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -112,10 +112,6 @@ func (q *Queries) Health(ctx context.Context) error { return q.client.Ping() } -type prepareDatabase interface { - Timetravel(d time.Duration) string -} - // cleanStaticQueries removes whitespaces, // such as ` `, \t, \n, from queries to improve // readability in logs and errors. @@ -152,3 +148,16 @@ func triggerBatch(ctx context.Context, handlers ...*handler.Handler) { wg.Wait() } + +func findTextEqualsQuery(column Column, queries []SearchQuery) (text string, ok bool) { + for _, query := range queries { + if query.Col() != column { + continue + } + tq, ok := query.(*textQuery) + if ok && tq.Compare == TextEquals { + return tq.Text, true + } + } + return "", false +} diff --git a/internal/query/quota.go b/internal/query/quota.go index 50bc28cabc..77d2f3892b 100644 --- a/internal/query/quota.go +++ b/internal/query/quota.go @@ -62,7 +62,7 @@ type Quota struct { func (q *Queries) GetQuota(ctx context.Context, instanceID string, unit quota.Unit) (qu *Quota, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareQuotaQuery(ctx, q.client) + query, scan := prepareQuotaQuery() stmt, args, err := query.Where( sq.Eq{ QuotaColumnInstanceID.identifier(): instanceID, @@ -79,7 +79,7 @@ func (q *Queries) GetQuota(ctx context.Context, instanceID string, unit quota.Un return qu, err } -func prepareQuotaQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Quota, error)) { +func prepareQuotaQuery() (sq.SelectBuilder, func(*sql.Row) (*Quota, error)) { return sq. Select( QuotaColumnID.identifier(), diff --git a/internal/query/quota_notifications.go b/internal/query/quota_notifications.go index 0015278b20..9e3cb1a10c 100644 --- a/internal/query/quota_notifications.go +++ b/internal/query/quota_notifications.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/quota" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -59,7 +58,7 @@ func (q *Queries) GetDueQuotaNotifications(ctx context.Context, instanceID strin ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() usedRel := uint16(math.Floor(float64(usedAbs*100) / float64(qu.Amount))) - query, scan := prepareQuotaNotificationsQuery(ctx, q.client) + query, scan := prepareQuotaNotificationsQuery() stmt, args, err := query.Where( sq.And{ sq.Eq{ @@ -149,7 +148,7 @@ func calculateThreshold(usedRel, notificationPercent uint16) uint16 { return uint16(times+percent-1)*100 + notificationPercent } -func prepareQuotaNotificationsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*QuotaNotifications, error)) { +func prepareQuotaNotificationsQuery() (sq.SelectBuilder, func(*sql.Rows) (*QuotaNotifications, error)) { return sq.Select( QuotaNotificationColumnID.identifier(), QuotaNotificationColumnCallURL.identifier(), @@ -157,7 +156,7 @@ func prepareQuotaNotificationsQuery(ctx context.Context, db prepareDatabase) (sq QuotaNotificationColumnRepeat.identifier(), QuotaNotificationColumnNextDueThreshold.identifier(), ). - From(quotaNotificationsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(quotaNotificationsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*QuotaNotifications, error) { cfgs := &QuotaNotifications{Configs: []*QuotaNotification{}} for rows.Next() { diff --git a/internal/query/quota_notifications_test.go b/internal/query/quota_notifications_test.go index a86b31df57..5515d2b3a0 100644 --- a/internal/query/quota_notifications_test.go +++ b/internal/query/quota_notifications_test.go @@ -92,8 +92,7 @@ var ( ` projections.quotas_notifications.percent,` + ` projections.quotas_notifications.repeat,` + ` projections.quotas_notifications.next_due_threshold` + - ` FROM projections.quotas_notifications` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` FROM projections.quotas_notifications`) quotaNotificationsCols = []string{ "id", @@ -175,7 +174,7 @@ func Test_prepareQuotaNotificationsQuery(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/quota_periods.go b/internal/query/quota_periods.go index 6ec42deba3..c954108c5f 100644 --- a/internal/query/quota_periods.go +++ b/internal/query/quota_periods.go @@ -7,7 +7,6 @@ import ( sq "github.com/Masterminds/squirrel" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/quota" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -40,7 +39,7 @@ var ( func (q *Queries) GetRemainingQuotaUsage(ctx context.Context, instanceID string, unit quota.Unit) (remaining *uint64, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareRemainingQuotaUsageQuery(ctx, q.client) + stmt, scan := prepareRemainingQuotaUsageQuery() query, args, err := stmt.Where( sq.And{ sq.Eq{ @@ -66,13 +65,13 @@ func (q *Queries) GetRemainingQuotaUsage(ctx context.Context, instanceID string, return remaining, err } -func prepareRemainingQuotaUsageQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*uint64, error)) { +func prepareRemainingQuotaUsageQuery() (sq.SelectBuilder, func(*sql.Row) (*uint64, error)) { return sq. Select( "greatest(0, " + QuotaColumnAmount.identifier() + "-" + QuotaPeriodColumnUsage.identifier() + ")", ). From(quotaPeriodsTable.identifier()). - Join(join(QuotaColumnUnit, QuotaPeriodColumnUnit) + db.Timetravel(call.Took(ctx))). + Join(join(QuotaColumnUnit, QuotaPeriodColumnUnit)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*uint64, error) { remaining := new(uint64) err := row.Scan(remaining) diff --git a/internal/query/quota_periods_test.go b/internal/query/quota_periods_test.go index 0f44c5e547..385a49c557 100644 --- a/internal/query/quota_periods_test.go +++ b/internal/query/quota_periods_test.go @@ -14,8 +14,7 @@ import ( var ( expectedRemainingQuotaUsageQuery = regexp.QuoteMeta(`SELECT greatest(0, projections.quotas.amount-projections.quotas_periods.usage)` + ` FROM projections.quotas_periods` + - ` JOIN projections.quotas ON projections.quotas_periods.unit = projections.quotas.unit AND projections.quotas_periods.instance_id = projections.quotas.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` JOIN projections.quotas ON projections.quotas_periods.unit = projections.quotas.unit AND projections.quotas_periods.instance_id = projections.quotas.instance_id`) remainingQuotaUsageCols = []string{ "usage", } @@ -84,7 +83,7 @@ func Test_prepareRemainingQuotaUsageQuery(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/quota_test.go b/internal/query/quota_test.go index a92938e0cb..1e3ff1e9b2 100644 --- a/internal/query/quota_test.go +++ b/internal/query/quota_test.go @@ -110,7 +110,7 @@ func Test_QuotaPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/restrictions.go b/internal/query/restrictions.go index 9e0dd37aa6..8cff5737f7 100644 --- a/internal/query/restrictions.go +++ b/internal/query/restrictions.go @@ -10,7 +10,6 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -72,7 +71,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareRestrictionsQuery(ctx, q.client) + stmt, scan := prepareRestrictionsQuery() instanceID := authz.GetInstance(ctx).InstanceID() query, args, err := stmt.Where(sq.Eq{ RestrictionsColumnInstanceID.identifier(): instanceID, @@ -92,7 +91,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res return restrictions, err } -func prepareRestrictionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (Restrictions, error)) { +func prepareRestrictionsQuery() (sq.SelectBuilder, func(*sql.Row) (Restrictions, error)) { return sq.Select( RestrictionsColumnAggregateID.identifier(), RestrictionsColumnCreationDate.identifier(), @@ -102,7 +101,7 @@ func prepareRestrictionsQuery(ctx context.Context, db prepareDatabase) (sq.Selec RestrictionsColumnDisallowPublicOrgRegistration.identifier(), RestrictionsColumnAllowedLanguages.identifier(), ). - From(restrictionsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(restrictionsTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (restrictions Restrictions, err error) { allowedLanguages := database.TextArray[string](make([]string, 0)) diff --git a/internal/query/restrictions_test.go b/internal/query/restrictions_test.go index cc7ee8442a..69ed81ef6d 100644 --- a/internal/query/restrictions_test.go +++ b/internal/query/restrictions_test.go @@ -21,8 +21,7 @@ var ( " projections.restrictions2.sequence," + " projections.restrictions2.disallow_public_org_registration," + " projections.restrictions2.allowed_languages" + - " FROM projections.restrictions2" + - " AS OF SYSTEM TIME '-1 ms'", + " FROM projections.restrictions2", ) restrictionsCols = []string{ @@ -115,7 +114,7 @@ func Test_RestrictionsPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.want.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.want.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/saml_request.go b/internal/query/saml_request.go index a0f6fdc6cd..784627bc59 100644 --- a/internal/query/saml_request.go +++ b/internal/query/saml_request.go @@ -5,13 +5,12 @@ import ( "database/sql" _ "embed" "errors" - "fmt" "time" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -28,9 +27,9 @@ type SamlRequest struct { Binding string } -func (a *SamlRequest) checkLoginClient(ctx context.Context) error { +func (a *SamlRequest) checkLoginClient(ctx context.Context, permissionCheck domain.PermissionCheck) error { if uid := authz.GetCtxData(ctx).UserID; uid != a.LoginClient { - return zerrors.ThrowPermissionDenied(nil, "OIDCv2-aL0ag", "Errors.SamlRequest.WrongLoginClient") + return permissionCheck(ctx, domain.PermissionSessionRead, authz.GetInstance(ctx).InstanceID(), "") } return nil } @@ -38,10 +37,6 @@ func (a *SamlRequest) checkLoginClient(ctx context.Context) error { //go:embed saml_request_by_id.sql var samlRequestByIDQuery string -func (q *Queries) samlRequestByIDQuery(ctx context.Context) string { - return fmt.Sprintf(samlRequestByIDQuery, q.client.Timetravel(call.Took(ctx))) -} - func (q *Queries) SamlRequestByID(ctx context.Context, shouldTriggerBulk bool, id string, checkLoginClient bool) (_ *SamlRequest, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -61,7 +56,7 @@ func (q *Queries) SamlRequestByID(ctx context.Context, shouldTriggerBulk bool, i &dst.ID, &dst.CreationDate, &dst.LoginClient, &dst.Issuer, &dst.ACS, &dst.RelayState, &dst.Binding, ) }, - q.samlRequestByIDQuery(ctx), + samlRequestByIDQuery, id, authz.GetInstance(ctx).InstanceID(), ) if errors.Is(err, sql.ErrNoRows) { @@ -72,7 +67,7 @@ func (q *Queries) SamlRequestByID(ctx context.Context, shouldTriggerBulk bool, i } if checkLoginClient { - if err = dst.checkLoginClient(ctx); err != nil { + if err = dst.checkLoginClient(ctx, q.checkPermission); err != nil { return nil, err } } diff --git a/internal/query/saml_request_by_id.sql b/internal/query/saml_request_by_id.sql index ac1c60058f..73eadb01ad 100644 --- a/internal/query/saml_request_by_id.sql +++ b/internal/query/saml_request_by_id.sql @@ -6,6 +6,6 @@ select acs, relay_state, binding -from projections.saml_requests %s +from projections.saml_requests where id = $1 and instance_id = $2 limit 1; diff --git a/internal/query/saml_request_test.go b/internal/query/saml_request_test.go index 5cf58369cb..3a062ac5fd 100644 --- a/internal/query/saml_request_test.go +++ b/internal/query/saml_request_test.go @@ -1,6 +1,7 @@ package query import ( + "context" "database/sql" "database/sql/driver" _ "embed" @@ -13,6 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -20,7 +22,6 @@ import ( func TestQueries_SamlRequestByID(t *testing.T) { expQuery := regexp.QuoteMeta(fmt.Sprintf( samlRequestByIDQuery, - asOfSystemTime, )) cols := []string{ @@ -38,11 +39,12 @@ func TestQueries_SamlRequestByID(t *testing.T) { checkLoginClient bool } tests := []struct { - name string - args args - expect sqlExpectation - want *SamlRequest - wantErr error + name string + args args + expect sqlExpectation + permissionCheck domain.PermissionCheck + want *SamlRequest + wantErr error }{ { name: "success, all values", @@ -89,7 +91,7 @@ func TestQueries_SamlRequestByID(t *testing.T) { wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ou8ue", "Errors.Internal"), }, { - name: "wrong login client", + name: "wrong login client/ not permitted", args: args{ shouldTriggerBulk: false, id: "123", @@ -104,16 +106,48 @@ func TestQueries_SamlRequestByID(t *testing.T) { "relayState", "binding", }, "123", "instanceID"), - wantErr: zerrors.ThrowPermissionDeniedf(nil, "OIDCv2-aL0ag", "Errors.SamlRequest.WrongLoginClient"), + permissionCheck: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + return zerrors.ThrowPermissionDenied(nil, "id", "not permitted") + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "id", "not permitted"), + }, + { + name: "wrong login client / permitted", + args: args{ + shouldTriggerBulk: false, + id: "123", + checkLoginClient: true, + }, + expect: mockQuery(expQuery, cols, []driver.Value{ + "id", + testNow, + "otherLoginClient", + "issuer", + "acs", + "relayState", + "binding", + }, "123", "instanceID"), + permissionCheck: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + return nil + }, + want: &SamlRequest{ + ID: "id", + CreationDate: testNow, + LoginClient: "otherLoginClient", + Issuer: "issuer", + ACS: "acs", + RelayState: "relayState", + Binding: "binding", + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { execMock(t, tt.expect, func(db *sql.DB) { q := &Queries{ + checkPermission: tt.permissionCheck, client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") diff --git a/internal/query/saml_sp.go b/internal/query/saml_sp.go new file mode 100644 index 0000000000..3682375d0b --- /dev/null +++ b/internal/query/saml_sp.go @@ -0,0 +1,104 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "errors" + "net/url" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type SAMLServiceProvider struct { + InstanceID string `json:"instance_id,omitempty"` + AppID string `json:"app_id,omitempty"` + State domain.AppState `json:"state,omitempty"` + EntityID string `json:"entity_id,omitempty"` + Metadata []byte `json:"metadata,omitempty"` + MetadataURL string `json:"metadata_url,omitempty"` + ProjectID string `json:"project_id,omitempty"` + ProjectRoleAssertion bool `json:"project_role_assertion,omitempty"` + LoginVersion domain.LoginVersion `json:"login_version,omitempty"` + LoginBaseURI *url.URL `json:"login_base_uri,omitempty"` +} + +//go:embed saml_sp_by_id.sql +var samlSPQuery string + +func (q *Queries) ActiveSAMLServiceProviderByID(ctx context.Context, entityID string) (sp *SAMLServiceProvider, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { + sp, err = scanSAMLServiceProviderByID(row) + return err + }, samlSPQuery, + authz.GetInstance(ctx).InstanceID(), + entityID, + ) + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-HeOcis2511", "Errors.App.NotFound") + } + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-OyJx1Rp30z", "Errors.Internal") + } + instance := authz.GetInstance(ctx) + loginV2 := instance.Features().LoginV2 + if loginV2.Required { + sp.LoginVersion = domain.LoginVersion2 + sp.LoginBaseURI = loginV2.BaseURI + } + return sp, err +} + +func scanSAMLServiceProviderByID(row *sql.Row) (*SAMLServiceProvider, error) { + var instanceID, appID, entityID, metadataURL, projectID sql.NullString + var projectRoleAssertion sql.NullBool + var metadata []byte + var state, loginVersion sql.NullInt16 + var loginBaseURI sql.NullString + + err := row.Scan( + &instanceID, + &appID, + &state, + &entityID, + &metadata, + &metadataURL, + &projectID, + &projectRoleAssertion, + &loginVersion, + &loginBaseURI, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-8cjj8ao6yY", "Errors.App.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-1xzFD209Bp", "Errors.Internal") + } + sp := &SAMLServiceProvider{ + InstanceID: instanceID.String, + AppID: appID.String, + State: domain.AppState(state.Int16), + EntityID: entityID.String, + Metadata: metadata, + MetadataURL: metadataURL.String, + ProjectID: projectID.String, + ProjectRoleAssertion: projectRoleAssertion.Bool, + } + if loginVersion.Valid { + sp.LoginVersion = domain.LoginVersion(loginVersion.Int16) + } + if loginBaseURI.Valid && loginBaseURI.String != "" { + url, err := url.Parse(loginBaseURI.String) + if err != nil { + return nil, err + } + sp.LoginBaseURI = url + } + return sp, nil +} diff --git a/internal/query/saml_sp_by_id.sql b/internal/query/saml_sp_by_id.sql new file mode 100644 index 0000000000..ff877c7ab9 --- /dev/null +++ b/internal/query/saml_sp_by_id.sql @@ -0,0 +1,19 @@ +select c.instance_id, + c.app_id, + a.state, + c.entity_id, + c.metadata, + c.metadata_url, + a.project_id, + p.project_role_assertion, + c.login_version, + c.login_base_uri +from projections.apps7_saml_configs c + join projections.apps7 a + on a.id = c.app_id and a.instance_id = c.instance_id and a.state = 1 + join projections.projects4 p + on p.id = a.project_id and p.instance_id = a.instance_id and p.state = 1 + join projections.orgs1 o + on o.id = p.resource_owner and o.instance_id = c.instance_id and o.org_state = 1 +where c.instance_id = $1 + and c.entity_id = $2 diff --git a/internal/query/saml_sp_test.go b/internal/query/saml_sp_test.go new file mode 100644 index 0000000000..35bf93c5fe --- /dev/null +++ b/internal/query/saml_sp_test.go @@ -0,0 +1,122 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + _ "embed" + "net/url" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestQueries_ActiveSAMLServiceProviderByID(t *testing.T) { + expQuery := regexp.QuoteMeta(samlSPQuery) + cols := []string{ + "instance_id", + "app_id", + "state", + "entity_id", + "metadata", + "metadata_url", + "project_id", + "project_role_assertion", + "login_version", + "login_base_uri", + } + + tests := []struct { + name string + mock sqlExpectation + want *SAMLServiceProvider + wantErr error + }{ + { + name: "no rows", + mock: mockQueryErr(expQuery, sql.ErrNoRows, "instanceID", "entityID"), + wantErr: zerrors.ThrowNotFound(sql.ErrNoRows, "QUERY-HeOcis2511", "Errors.App.NotFound"), + }, + { + name: "internal error", + mock: mockQueryErr(expQuery, sql.ErrConnDone, "instanceID", "entityID"), + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-OyJx1Rp30z", "Errors.Internal"), + }, + { + name: "sp", + mock: mockQuery(expQuery, cols, []driver.Value{ + "230690539048009730", + "236647088211886082", + domain.AppStateActive, + "https://test.com/metadata", + "metadata", + "https://test.com/metadata", + "236645808328409090", + true, + domain.LoginVersionUnspecified, + "", + }, "instanceID", "entityID"), + want: &SAMLServiceProvider{ + InstanceID: "230690539048009730", + AppID: "236647088211886082", + State: domain.AppStateActive, + EntityID: "https://test.com/metadata", + Metadata: []byte("metadata"), + MetadataURL: "https://test.com/metadata", + ProjectID: "236645808328409090", + ProjectRoleAssertion: true, + }, + }, + { + name: "sp with loginversion", + mock: mockQuery(expQuery, cols, []driver.Value{ + "230690539048009730", + "236647088211886082", + domain.AppStateActive, + "https://test.com/metadata", + "metadata", + "https://test.com/metadata", + "236645808328409090", + true, + domain.LoginVersion2, + "https://test.com/login", + }, "instanceID", "entityID"), + want: &SAMLServiceProvider{ + InstanceID: "230690539048009730", + AppID: "236647088211886082", + State: domain.AppStateActive, + EntityID: "https://test.com/metadata", + Metadata: []byte("metadata"), + MetadataURL: "https://test.com/metadata", + ProjectID: "236645808328409090", + ProjectRoleAssertion: true, + LoginVersion: domain.LoginVersion2, + LoginBaseURI: func() *url.URL { + ret, _ := url.Parse("https://test.com/login") + return ret + }(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + execMock(t, tt.mock, func(db *sql.DB) { + q := &Queries{ + client: &database.DB{ + DB: db, + }, + } + ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") + got, err := q.ActiveSAMLServiceProviderByID(ctx, "entityID") + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + }) + } +} diff --git a/internal/query/secret_generator_test.go b/internal/query/secret_generator_test.go index 683dc3441e..9ce8e71769 100644 --- a/internal/query/secret_generator_test.go +++ b/internal/query/secret_generator_test.go @@ -26,8 +26,7 @@ var ( ` projections.secret_generators2.include_upper_letters,` + ` projections.secret_generators2.include_digits,` + ` projections.secret_generators2.include_symbols` + - ` FROM projections.secret_generators2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.secret_generators2` prepareSecretGeneratorCols = []string{ "aggregate_id", "generator_type", @@ -55,8 +54,7 @@ var ( ` projections.secret_generators2.include_digits,` + ` projections.secret_generators2.include_symbols,` + ` COUNT(*) OVER ()` + - ` FROM projections.secret_generators2` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.secret_generators2` prepareSecretGeneratorsCols = []string{ "aggregate_id", "generator_type", @@ -312,7 +310,7 @@ func Test_SecretGeneratorsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/secret_generators.go b/internal/query/secret_generators.go index 8ee8694d2b..c267d7b290 100644 --- a/internal/query/secret_generators.go +++ b/internal/query/secret_generators.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -127,7 +126,7 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai defer func() { span.EndWithError(err) }() instanceID := authz.GetInstance(ctx).InstanceID() - stmt, scan := prepareSecretGeneratorQuery(ctx, q.client) + stmt, scan := prepareSecretGeneratorQuery() query, args, err := stmt.Where(sq.Eq{ SecretGeneratorColumnGeneratorType.identifier(): generatorType, SecretGeneratorColumnInstanceID.identifier(): instanceID, @@ -148,7 +147,7 @@ func (q *Queries) SearchSecretGenerators(ctx context.Context, queries *SecretGen ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSecretGeneratorsQuery(ctx, q.client) + query, scan := prepareSecretGeneratorsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SecretGeneratorColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -180,7 +179,7 @@ func NewSecretGeneratorTypeSearchQuery(value int32) (SearchQuery, error) { return NewNumberQuery(SecretGeneratorColumnGeneratorType, value, NumberEquals) } -func prepareSecretGeneratorQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SecretGenerator, error)) { +func prepareSecretGeneratorQuery() (sq.SelectBuilder, func(*sql.Row) (*SecretGenerator, error)) { return sq.Select( SecretGeneratorColumnAggregateID.identifier(), SecretGeneratorColumnGeneratorType.identifier(), @@ -194,7 +193,7 @@ func prepareSecretGeneratorQuery(ctx context.Context, db prepareDatabase) (sq.Se SecretGeneratorColumnIncludeUpperLetters.identifier(), SecretGeneratorColumnIncludeDigits.identifier(), SecretGeneratorColumnIncludeSymbols.identifier()). - From(secretGeneratorsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(secretGeneratorsTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SecretGenerator, error) { secretGenerator := new(SecretGenerator) @@ -222,7 +221,7 @@ func prepareSecretGeneratorQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareSecretGeneratorsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*SecretGenerators, error)) { +func prepareSecretGeneratorsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SecretGenerators, error)) { return sq.Select( SecretGeneratorColumnAggregateID.identifier(), SecretGeneratorColumnGeneratorType.identifier(), @@ -237,7 +236,7 @@ func prepareSecretGeneratorsQuery(ctx context.Context, db prepareDatabase) (sq.S SecretGeneratorColumnIncludeDigits.identifier(), SecretGeneratorColumnIncludeSymbols.identifier(), countColumn.identifier()). - From(secretGeneratorsTable.identifier() + db.Timetravel(call.Took(ctx))). + From(secretGeneratorsTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*SecretGenerators, error) { secretGenerators := make([]*SecretGenerator, 0) diff --git a/internal/query/security_policy.go b/internal/query/security_policy.go index 51938abdae..7a3fb3fa89 100644 --- a/internal/query/security_policy.go +++ b/internal/query/security_policy.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/zerrors" @@ -63,7 +62,7 @@ type SecurityPolicy struct { } func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, err error) { - stmt, scan := prepareSecurityPolicyQuery(ctx, q.client) + stmt, scan := prepareSecurityPolicyQuery() query, args, err := stmt.Where(sq.Eq{ SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() @@ -78,7 +77,7 @@ func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, e return policy, err } -func prepareSecurityPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SecurityPolicy, error)) { +func prepareSecurityPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*SecurityPolicy, error)) { return sq.Select( SecurityPolicyColumnInstanceID.identifier(), SecurityPolicyColumnCreationDate.identifier(), @@ -88,7 +87,7 @@ func prepareSecurityPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sel SecurityPolicyColumnEnableIframeEmbedding.identifier(), SecurityPolicyColumnAllowedOrigins.identifier(), SecurityPolicyColumnEnableImpersonation.identifier()). - From(securityPolicyTable.identifier() + db.Timetravel(call.Took(ctx))). + From(securityPolicyTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SecurityPolicy, error) { securityPolicy := new(SecurityPolicy) diff --git a/internal/query/session.go b/internal/query/session.go index d30fe4cda9..ff0cbd8d42 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -13,7 +13,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -114,6 +113,24 @@ func sessionCheckPermission(ctx context.Context, resourceOwner string, creator s return permissionCheck(ctx, domain.PermissionSessionRead, resourceOwner, "") } +func sessionsPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + SessionColumnResourceOwner, + domain.PermissionSessionRead, + // Allow if user is creator + OwnedRowsPermissionOption(SessionColumnCreator), + // Allow if session belongs to the user + OwnedRowsPermissionOption(SessionColumnUserID), + // Allow if session belongs to the same useragent + ConnectionPermissionOption(SessionColumnUserAgentFingerprintID, authz.GetCtxData(ctx).AgentID), + ) + return query.JoinClause(join, args...) +} + func (q *SessionsSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { @@ -235,6 +252,10 @@ func (q *Queries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, s return nil, err } if sessionToken == "" { + // for internal calls, no token or permission check is necessary + if permissionCheck == nil { + return session, nil + } if err := sessionCheckPermission(ctx, session.ResourceOwner, session.Creator, session.UserAgent, session.UserFactor, permissionCheck); err != nil { return nil, err } @@ -257,7 +278,7 @@ func (q *Queries) sessionByID(ctx context.Context, shouldTriggerBulk bool, id st traceSpan.EndWithError(err) } - query, scan := prepareSessionQuery(ctx, q.client) + query, scan := prepareSessionQuery() stmt, args, err := query.Where( sq.Eq{ SessionColumnID.identifier(): id, @@ -279,21 +300,23 @@ func (q *Queries) sessionByID(ctx context.Context, shouldTriggerBulk bool, id st } func (q *Queries) SearchSessions(ctx context.Context, queries *SessionsSearchQueries, permissionCheck domain.PermissionCheck) (*Sessions, error) { - sessions, err := q.searchSessions(ctx, queries) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + sessions, err := q.searchSessions(ctx, queries, permissionCheckV2) if err != nil { return nil, err } - if permissionCheck != nil { + if permissionCheck != nil && !permissionCheckV2 { sessionsCheckPermission(ctx, sessions, permissionCheck) } return sessions, nil } -func (q *Queries) searchSessions(ctx context.Context, queries *SessionsSearchQueries) (sessions *Sessions, err error) { +func (q *Queries) searchSessions(ctx context.Context, queries *SessionsSearchQueries, permissionCheckV2 bool) (sessions *Sessions, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSessionsQuery(ctx, q.client) + query, scan := prepareSessionsQuery() + query = sessionsPermissionCheckV2(ctx, query, permissionCheckV2) stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SessionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -339,7 +362,7 @@ func NewCreationDateQuery(datetime time.Time, compare TimestampComparison) (Sear return NewTimestampQuery(SessionColumnCreationDate, datetime, compare) } -func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, string, error)) { +func prepareSessionQuery() (sq.SelectBuilder, func(*sql.Row) (*Session, string, error)) { return sq.Select( SessionColumnID.identifier(), SessionColumnCreationDate.identifier(), @@ -370,7 +393,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil ).From(sessionsTable.identifier()). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(HumanUserIDCol, SessionColumnUserID)). - LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(UserIDCol, SessionColumnUserID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Session, string, error) { session := new(Session) @@ -452,7 +475,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Sessions, error)) { +func prepareSessionsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Sessions, error)) { return sq.Select( SessionColumnID.identifier(), SessionColumnCreationDate.identifier(), @@ -483,7 +506,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui ).From(sessionsTable.identifier()). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(HumanUserIDCol, SessionColumnUserID)). - LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(UserIDCol, SessionColumnUserID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Sessions, error) { sessions := &Sessions{Sessions: []*Session{}} diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index ba897e6062..e0d9cfda71 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -50,8 +50,7 @@ var ( ` FROM projections.sessions8` + ` LEFT JOIN projections.login_names3 ON projections.sessions8.user_id = projections.login_names3.user_id AND projections.sessions8.instance_id = projections.login_names3.instance_id` + ` LEFT JOIN projections.users14_humans ON projections.sessions8.user_id = projections.users14_humans.user_id AND projections.sessions8.instance_id = projections.users14_humans.instance_id` + - ` LEFT JOIN projections.users14 ON projections.sessions8.user_id = projections.users14.id AND projections.sessions8.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.users14 ON projections.sessions8.user_id = projections.users14.id AND projections.sessions8.instance_id = projections.users14.instance_id`) expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions8.id,` + ` projections.sessions8.creation_date,` + ` projections.sessions8.change_date,` + @@ -81,8 +80,7 @@ var ( ` FROM projections.sessions8` + ` LEFT JOIN projections.login_names3 ON projections.sessions8.user_id = projections.login_names3.user_id AND projections.sessions8.instance_id = projections.login_names3.instance_id` + ` LEFT JOIN projections.users14_humans ON projections.sessions8.user_id = projections.users14_humans.user_id AND projections.sessions8.instance_id = projections.users14_humans.instance_id` + - ` LEFT JOIN projections.users14 ON projections.sessions8.user_id = projections.users14.id AND projections.sessions8.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.users14 ON projections.sessions8.user_id = projections.users14.id AND projections.sessions8.instance_id = projections.users14.instance_id`) sessionCols = []string{ "id", @@ -440,7 +438,7 @@ func Test_SessionsPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -577,14 +575,14 @@ func Test_SessionPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } -func prepareSessionQueryTesting(t *testing.T, token string) func(context.Context, prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, error)) { - return func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Session, error)) { - builder, scan := prepareSessionQuery(ctx, db) +func prepareSessionQueryTesting(t *testing.T, token string) func() (sq.SelectBuilder, func(*sql.Row) (*Session, error)) { + return func() (sq.SelectBuilder, func(*sql.Row) (*Session, error)) { + builder, scan := prepareSessionQuery() return builder, func(row *sql.Row) (*Session, error) { session, tokenID, err := scan(row) require.Equal(t, tokenID, token) diff --git a/internal/query/sms.go b/internal/query/sms.go index 310d3d0f14..3659f05daf 100644 --- a/internal/query/sms.go +++ b/internal/query/sms.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -149,7 +148,7 @@ func (q *Queries) SMSProviderConfigByID(ctx context.Context, id string) (config ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSMSConfigQuery(ctx, q.client) + query, scan := prepareSMSConfigQuery() stmt, args, err := query.Where( sq.Eq{ SMSColumnID.identifier(): id, @@ -171,7 +170,7 @@ func (q *Queries) SMSProviderConfigActive(ctx context.Context, instanceID string ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSMSConfigQuery(ctx, q.client) + query, scan := prepareSMSConfigQuery() stmt, args, err := query.Where( sq.Eq{ SMSColumnInstanceID.identifier(): instanceID, @@ -193,7 +192,7 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSMSConfigsQuery(ctx, q.client) + query, scan := prepareSMSConfigsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SMSColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -217,7 +216,7 @@ func NewSMSProviderStateQuery(state domain.SMSConfigState) (SearchQuery, error) return NewNumberQuery(SMSColumnState, state, NumberEquals) } -func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SMSConfig, error)) { +func prepareSMSConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMSConfig, error)) { return sq.Select( SMSColumnID.identifier(), SMSColumnAggregateID.identifier(), @@ -238,7 +237,7 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu SMSHTTPColumnEndpoint.identifier(), ).From(smsConfigsTable.identifier()). LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). - LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SMSConfig, error) { config := new(SMSConfig) @@ -281,7 +280,7 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*SMSConfigs, error)) { +func prepareSMSConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMSConfigs, error)) { return sq.Select( SMSColumnID.identifier(), SMSColumnAggregateID.identifier(), @@ -304,7 +303,7 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB countColumn.identifier(), ).From(smsConfigsTable.identifier()). LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). - LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Rows) (*SMSConfigs, error) { configs := &SMSConfigs{Configs: []*SMSConfig{}} diff --git a/internal/query/sms_test.go b/internal/query/sms_test.go index 82c3659f2c..e6e79d72bc 100644 --- a/internal/query/sms_test.go +++ b/internal/query/sms_test.go @@ -35,8 +35,7 @@ var ( ` projections.sms_configs3_http.endpoint` + ` FROM projections.sms_configs3` + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + - ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id`) expectedSMSConfigsQuery = regexp.QuoteMeta(`SELECT projections.sms_configs3.id,` + ` projections.sms_configs3.aggregate_id,` + ` projections.sms_configs3.creation_date,` + @@ -59,8 +58,7 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.sms_configs3` + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + - ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id`) smsConfigCols = []string{ "id", @@ -353,7 +351,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } @@ -494,7 +492,7 @@ func Test_SMSConfigPrepare(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/smtp.go b/internal/query/smtp.go index 7c45fe33fe..4238ec121e 100644 --- a/internal/query/smtp.go +++ b/internal/query/smtp.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -153,7 +152,7 @@ func (q *Queries) SMTPConfigActive(ctx context.Context, resourceOwner string) (c ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareSMTPConfigQuery(ctx, q.client) + stmt, scan := prepareSMTPConfigQuery() query, args, err := stmt.Where(sq.Eq{ SMTPConfigColumnResourceOwner.identifier(): resourceOwner, SMTPConfigColumnInstanceID.identifier(): resourceOwner, @@ -174,7 +173,7 @@ func (q *Queries) SMTPConfigByID(ctx context.Context, instanceID, id string) (co ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareSMTPConfigQuery(ctx, q.client) + stmt, scan := prepareSMTPConfigQuery() query, args, err := stmt.Where(sq.Eq{ SMTPConfigColumnInstanceID.identifier(): instanceID, SMTPConfigColumnID.identifier(): id, @@ -190,7 +189,7 @@ func (q *Queries) SMTPConfigByID(ctx context.Context, instanceID, id string) (co return config, err } -func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, error)) { +func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, error)) { password := new(crypto.CryptoValue) return sq.Select( @@ -215,7 +214,7 @@ func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectB SMTPConfigHTTPColumnEndpoint.identifier()). From(smtpConfigsTable.identifier()). LeftJoin(join(SMTPConfigSMTPColumnID, SMTPConfigColumnID)). - LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SMTPConfig, error) { config := new(SMTPConfig) @@ -255,7 +254,7 @@ func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } } -func prepareSMTPConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, error)) { +func prepareSMTPConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, error)) { return sq.Select( SMTPConfigColumnCreationDate.identifier(), SMTPConfigColumnChangeDate.identifier(), @@ -279,7 +278,7 @@ func prepareSMTPConfigsQuery(ctx context.Context, db prepareDatabase) (sq.Select countColumn.identifier(), ).From(smtpConfigsTable.identifier()). LeftJoin(join(SMTPConfigSMTPColumnID, SMTPConfigColumnID)). - LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID)). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*SMTPConfigs, error) { configs := &SMTPConfigs{Configs: []*SMTPConfig{}} @@ -329,7 +328,7 @@ func (q *Queries) SearchSMTPConfigs(ctx context.Context, queries *SMTPConfigsSea ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareSMTPConfigsQuery(ctx, q.client) + query, scan := prepareSMTPConfigsQuery() stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SMTPConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), diff --git a/internal/query/smtp_test.go b/internal/query/smtp_test.go index 4d12edcbd3..68ace249aa 100644 --- a/internal/query/smtp_test.go +++ b/internal/query/smtp_test.go @@ -33,8 +33,7 @@ var ( ` projections.smtp_configs5_http.endpoint` + ` FROM projections.smtp_configs5` + ` LEFT JOIN projections.smtp_configs5_smtp ON projections.smtp_configs5.id = projections.smtp_configs5_smtp.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_smtp.instance_id` + - ` LEFT JOIN projections.smtp_configs5_http ON projections.smtp_configs5.id = projections.smtp_configs5_http.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_http.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.smtp_configs5_http ON projections.smtp_configs5.id = projections.smtp_configs5_http.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_http.instance_id` prepareSMTPConfigCols = []string{ "creation_date", "change_date", @@ -287,7 +286,7 @@ func Test_SMTPConfigsPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/system_features.go b/internal/query/system_features.go index 31ad402d12..dcbbb7d6fe 100644 --- a/internal/query/system_features.go +++ b/internal/query/system_features.go @@ -25,7 +25,6 @@ type SystemFeatures struct { LegacyIntrospection FeatureSource[bool] UserSchema FeatureSource[bool] TokenExchange FeatureSource[bool] - Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] OIDCSingleV1SessionTermination FeatureSource[bool] DisableUserTokenEvent FeatureSource[bool] diff --git a/internal/query/system_features_model.go b/internal/query/system_features_model.go index 217154e3ed..69e1f35968 100644 --- a/internal/query/system_features_model.go +++ b/internal/query/system_features_model.go @@ -60,7 +60,6 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.SystemLegacyIntrospectionEventType, feature_v2.SystemUserSchemaEventType, feature_v2.SystemTokenExchangeEventType, - feature_v2.SystemActionsEventType, feature_v2.SystemImprovedPerformanceEventType, feature_v2.SystemOIDCSingleV1SessionTerminationEventType, feature_v2.SystemDisableUserTokenEvent, @@ -82,7 +81,8 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S return err } switch key { - case feature.KeyUnspecified: + case feature.KeyUnspecified, + feature.KeyActionsDeprecated: return nil case feature.KeyLoginDefaultOrg: features.LoginDefaultOrg.set(level, event.Value) @@ -94,8 +94,6 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S features.UserSchema.set(level, event.Value) case feature.KeyTokenExchange: features.TokenExchange.set(level, event.Value) - case feature.KeyActions: - features.Actions.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) case feature.KeyOIDCSingleV1SessionTermination: diff --git a/internal/query/system_features_test.go b/internal/query/system_features_test.go index e460d38cec..5a58ac23d7 100644 --- a/internal/query/system_features_test.go +++ b/internal/query/system_features_test.go @@ -45,26 +45,22 @@ func TestQueries_GetSystemFeatures(t *testing.T) { name: "all features set", eventstore: expectEventstore( expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, true, - )), ), ), want: &SystemFeatures{ @@ -87,41 +83,33 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelSystem, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, }, }, { name: "all features set, reset, set some feature", eventstore: expectEventstore( expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( context.Background(), aggregate, feature_v2.SystemResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), @@ -147,41 +135,33 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, { name: "all features set, reset, set some feature, not cascaded", eventstore: expectEventstore( expectFilter( - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemLegacyIntrospectionEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemActionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( context.Background(), aggregate, feature_v2.SystemResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( + eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, )), @@ -207,10 +187,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - Actions: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, } diff --git a/internal/query/target.go b/internal/query/target.go index 03db85236c..d9b50f4a14 100644 --- a/internal/query/target.go +++ b/internal/query/target.go @@ -116,8 +116,8 @@ func (q *Queries) SearchTargets(ctx context.Context, queries *TargetSearchQuerie eq := sq.Eq{ TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareTargetsQuery(ctx, q.client) - targets, err := genericRowsQueryWithState[*Targets](ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan) + query, scan := prepareTargetsQuery() + targets, err := genericRowsQueryWithState(ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan) if err != nil { return nil, err } @@ -134,8 +134,8 @@ func (q *Queries) GetTargetByID(ctx context.Context, id string) (*Target, error) TargetColumnID.identifier(): id, TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } - query, scan := prepareTargetQuery(ctx, q.client) - target, err := genericRowQuery[*Target](ctx, q.client, query.Where(eq), scan) + query, scan := prepareTargetQuery() + target, err := genericRowQuery(ctx, q.client, query.Where(eq), scan) if err != nil { return nil, err } @@ -153,7 +153,7 @@ func NewTargetInIDsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(TargetColumnID, values) } -func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, error)) { +func prepareTargetsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, error)) { return sq.Select( TargetColumnID.identifier(), TargetColumnCreationDate.identifier(), @@ -205,7 +205,7 @@ func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fu } } -func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Target, error)) { +func prepareTargetQuery() (sq.SelectBuilder, func(row *sql.Row) (*Target, error)) { return sq.Select( TargetColumnID.identifier(), TargetColumnCreationDate.identifier(), diff --git a/internal/query/target_test.go b/internal/query/target_test.go index aa1ad517b7..ef564bf236 100644 --- a/internal/query/target_test.go +++ b/internal/query/target_test.go @@ -372,7 +372,7 @@ func Test_TargetPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/testdata/oidc_client_jwt_loginversion.json b/internal/query/testdata/oidc_client_jwt_loginversion.json new file mode 100644 index 0000000000..7664c0abc1 --- /dev/null +++ b/internal/query/testdata/oidc_client_jwt_loginversion.json @@ -0,0 +1,32 @@ +{ + "instance_id": "230690539048009730", + "app_id": "236647088211886082", + "state": 1, + "client_id": "236647088211951618", + "client_secret": null, + "redirect_uris": ["http://localhost:9999/auth/callback"], + "response_types": [0], + "grant_types": [0, 2], + "application_type": 0, + "auth_method_type": 3, + "post_logout_redirect_uris": ["https://example.com/logout"], + "is_dev_mode": true, + "access_token_type": 1, + "access_token_role_assertion": true, + "id_token_role_assertion": true, + "id_token_userinfo_assertion": true, + "clock_skew": 1000000000, + "additional_origins": ["https://example.com"], + "project_id": "236645808328409090", + "project_role_assertion": true, + "project_role_keys": ["role1", "role2"], + "public_keys": { + "236647201860747266": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFB\nT0NBUThBTUlJQkNnS0NBUUVBMnVmQUwxYjcyYkl5MWFyK1dzNmIKR29oSkpRRkI3ZGZSYXBEcWVx\nTThVa3A2Q1ZkUHpxL3BPejF2aUFxNTB5eldaSnJ5Risyd3NoRkFLR0Y5QTIvQgoyWWY5YkpYUFov\nS2JrRnJZVDNOVHZZRGt2bGFTVGw5bU1uenJVMjlzNDhGMVBUV0tmQitDM2FNc09FRzFCdWZWCnM2\nM3FGNG5yRVBqU2JobGpJY285RlpxNFhwcEl6aE1RMGZEZEEvK1h5Z0NKcXZ1YUwwTGliTTFLcmxV\nZG51NzEKWWVraFNKakVQbnZPaXNYSWs0SVh5d29HSU93dGp4a0R2Tkl0UXZhTVZsZHI0L2tiNnV2\nYmdkV3dxNUV3QlpYcQpsb3cya3lKb3YzOFY0VWsySThrdVhwTGNucnB3NVRpbzJvb2lVRTI3YjB2\nSFpxQktPZWk5VW84OHFDcm4zRUt4CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0t\nLS0K" + }, + "settings": { + "access_token_lifetime": 43200000000000, + "id_token_lifetime": 43200000000000 + }, + "login_version": 1, + "login_base_uri": "https://test.com/login" +} diff --git a/internal/query/user.go b/internal/query/user.go index bb76e51f66..a97e3bbd14 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -13,7 +13,6 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" @@ -133,6 +132,20 @@ func usersCheckPermission(ctx context.Context, users *Users, permissionCheck dom ) } +func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + UserResourceOwnerCol, + domain.PermissionUserRead, + SingleOrgPermissionOption(queries.Queries), + OwnedRowsPermissionOption(UserIDCol), + ) + return query.JoinClause(join, args...) +} + type UserSearchQueries struct { SearchRequest Queries []SearchQuery @@ -428,39 +441,11 @@ func (q *Queries) GetUserByLoginName(ctx context.Context, shouldTriggered bool, return user, err } -// Deprecated: use either GetUserByID or GetUserByLoginName -func (q *Queries) GetUser(ctx context.Context, shouldTriggerBulk bool, queries ...SearchQuery) (user *User, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - if shouldTriggerBulk { - triggerUserProjections(ctx) - } - - query, scan := prepareUserQuery(ctx, q.client) - for _, q := range queries { - query = q.toQuery(query) - } - eq := sq.Eq{ - UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), - } - stmt, args, err := query.Where(eq).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dnhr2", "Errors.Query.SQLStatment") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - user, err = scan(row) - return err - }, stmt, args...) - return user, err -} - func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries ...SearchQuery) (profile *Profile, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareProfileQuery(ctx, q.client) + query, scan := prepareProfileQuery() for _, q := range queries { query = q.toQuery(query) } @@ -484,7 +469,7 @@ func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...S ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareEmailQuery(ctx, q.client) + query, scan := prepareEmailQuery() for _, q := range queries { query = q.toQuery(query) } @@ -508,7 +493,7 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := preparePhoneQuery(ctx, q.client) + query, scan := preparePhoneQuery() for _, q := range queries { query = q.toQuery(query) } @@ -595,7 +580,7 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri triggerUserProjections(ctx) } - query, scan := prepareNotifyUserQuery(ctx, q.client) + query, scan := prepareNotifyUserQuery() for _, q := range queries { query = q.toQuery(query) } @@ -636,7 +621,8 @@ func (q *Queries) CountUsers(ctx context.Context, queries *UserSearchQueries) (c } func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheck domain.PermissionCheck) (*Users, error) { - users, err := q.searchUsers(ctx, queries, permissionCheck != nil && authz.GetFeatures(ctx).PermissionCheckV2) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + users, err := q.searchUsers(ctx, queries, permissionCheckV2) if err != nil { return nil, err } @@ -650,15 +636,11 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUsersQuery(ctx, q.client) - query = queries.toQuery(query).Where(sq.Eq{ + query, scan := prepareUsersQuery() + query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries) + stmt, args, err := queries.toQuery(query).Where(sq.Eq{ UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), - }) - if permissionCheckV2 { - query = wherePermittedOrgs(ctx, query, UserResourceOwnerCol.identifier(), domain.PermissionUserRead) - } - - stmt, args, err := query.ToSql() + }).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") } @@ -678,7 +660,7 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserUniqueQuery(ctx, q.client) + query, scan := prepareUserUniqueQuery() queries := make([]SearchQuery, 0, 3) if username != "" { usernameQuery, err := NewUserUsernameSearchQuery(username, TextEquals) @@ -737,15 +719,19 @@ func (r *UserSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { func NewUserOrSearchQuery(values []SearchQuery) (SearchQuery, error) { return NewOrQuery(values...) } + func NewUserAndSearchQuery(values []SearchQuery) (SearchQuery, error) { return NewAndQuery(values...) } + func NewUserNotSearchQuery(value SearchQuery) (SearchQuery, error) { return NewNotQuery(value) } + func NewUserInUserIdsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(UserIDCol, values) } + func NewUserInUserEmailsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(HumanEmailCol, values) } @@ -807,7 +793,7 @@ func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) { } func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) { - //linking queries for the subselect + // linking queries for the subselect instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals) if err != nil { return nil, err @@ -816,12 +802,12 @@ func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (Searc if err != nil { return nil, err } - //text query to select data from the linked sub select + // text query to select data from the linked sub select loginNameQuery, err := NewTextQuery(LoginNameNameCol, value, comparison) if err != nil { return nil, err } - //full definition of the sub select + // full definition of the sub select subSelect, err := NewSubSelect(LoginNameUserIDCol, []SearchQuery{instanceQuery, userIDQuery, loginNameQuery}) if err != nil { return nil, err @@ -961,7 +947,7 @@ func scanUser(row *sql.Row) (*User, error) { return u, nil } -func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*User, error)) { +func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() if err != nil { return sq.SelectBuilder{}, nil @@ -1012,14 +998,14 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder loginNamesArgs...). LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)), + userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), preferredLoginNameArgs...). PlaceholderFormat(sq.Dollar), scanUser } -func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { +func prepareProfileQuery() (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1035,7 +1021,7 @@ func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil HumanGenderCol.identifier(), HumanAvatarURLCol.identifier()). From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(HumanUserIDCol, UserIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Profile, error) { p := new(Profile) @@ -1085,7 +1071,7 @@ func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareEmailQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Email, error)) { +func prepareEmailQuery() (sq.SelectBuilder, func(*sql.Row) (*Email, error)) { return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1096,7 +1082,7 @@ func prepareEmailQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde HumanEmailCol.identifier(), HumanIsEmailVerifiedCol.identifier()). From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(HumanUserIDCol, UserIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Email, error) { e := new(Email) @@ -1132,7 +1118,7 @@ func prepareEmailQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde } } -func preparePhoneQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { +func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1143,7 +1129,7 @@ func preparePhoneQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde HumanPhoneCol.identifier(), HumanIsPhoneVerifiedCol.identifier()). From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(HumanUserIDCol, UserIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Phone, error) { e := new(Phone) @@ -1179,7 +1165,7 @@ func preparePhoneQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde } } -func prepareNotifyUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { +func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() if err != nil { return sq.SelectBuilder{}, nil @@ -1224,7 +1210,7 @@ func prepareNotifyUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectB loginNamesArgs...). LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)), + userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), preferredLoginNameArgs...). PlaceholderFormat(sq.Dollar), scanNotifyUser @@ -1331,7 +1317,7 @@ func prepareCountUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (uint64, error) } } -func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (bool, error)) { +func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { return sq.Select( UserIDCol.identifier(), UserStateCol.identifier(), @@ -1340,7 +1326,7 @@ func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectB HumanEmailCol.identifier(), HumanIsEmailVerifiedCol.identifier()). From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(HumanUserIDCol, UserIDCol)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (bool, error) { userID := sql.NullString{} @@ -1368,7 +1354,7 @@ func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } } -func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { +func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() if err != nil { return sq.SelectBuilder{}, nil @@ -1417,7 +1403,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde loginNamesArgs...). LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)), + userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), preferredLoginNameArgs...). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Users, error) { diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index 0687545aef..fce34967cf 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -3,6 +3,7 @@ package query import ( "context" "database/sql" + _ "embed" "errors" "slices" "time" @@ -11,7 +12,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -104,6 +104,19 @@ func authMethodsCheckPermission(ctx context.Context, methods *AuthMethods, permi ) } +func userAuthMethodPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + UserAuthMethodColumnResourceOwner, + domain.PermissionUserRead, + OwnedRowsPermissionOption(UserIDCol), + ) + return query.JoinClause(join, args...) +} + type AuthMethod struct { UserID string CreationDate time.Time @@ -137,11 +150,12 @@ func (q *UserAuthMethodSearchQueries) hasUserID() bool { } func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, permissionCheck domain.PermissionCheck) (userAuthMethods *AuthMethods, err error) { - methods, err := q.searchUserAuthMethods(ctx, queries) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + methods, err := q.searchUserAuthMethods(ctx, queries, permissionCheckV2) if err != nil { return nil, err } - if permissionCheck != nil && len(methods.AuthMethods) > 0 { + if permissionCheck != nil && len(methods.AuthMethods) > 0 && !permissionCheckV2 { // when userID for query is provided, only one check has to be done if queries.hasUserID() { if err := userCheckPermission(ctx, methods.AuthMethods[0].ResourceOwner, methods.AuthMethods[0].UserID, permissionCheck); err != nil { @@ -154,11 +168,12 @@ func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMe return methods, nil } -func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries) (userAuthMethods *AuthMethods, err error) { +func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, permissionCheckV2 bool) (userAuthMethods *AuthMethods, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserAuthMethodsQuery(ctx, q.client) + query, scan := prepareUserAuthMethodsQuery() + query = userAuthMethodPermissionCheckV2(ctx, query, permissionCheckV2) stmt, args, err := queries.toQuery(query).Where(sq.Eq{UserAuthMethodColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-j9NJd", "Errors.Query.InvalidRequest") @@ -185,7 +200,7 @@ func (q *Queries) ListUserAuthMethodTypes(ctx context.Context, userID string, ac ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserAuthMethodTypesQuery(ctx, q.client, activeOnly, includeWithoutDomain, queryDomain) + query, scan := prepareUserAuthMethodTypesQuery(activeOnly, includeWithoutDomain, queryDomain) eq := sq.Eq{ UserIDCol.identifier(): userID, UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -212,6 +227,9 @@ type UserAuthMethodRequirements struct { ForceMFALocalOnly bool } +//go:embed user_auth_method_types_required.sql +var listUserAuthMethodTypesStmt string + func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string) (requirements *UserAuthMethodRequirements, err error) { ctxData := authz.GetCtxData(ctx) if ctxData.UserID != userID { @@ -222,20 +240,33 @@ func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID st ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, q.client) - eq := sq.Eq{ - UserIDCol.identifier(): userID, - UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), - } - stmt, args, err := query.Where(eq).ToSql() - if err != nil { - return nil, zerrors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - requirements, err = scan(row) - return err - }, stmt, args...) + err = q.client.QueryRowContext(ctx, + func(row *sql.Row) error { + var userType sql.NullInt32 + var forceMFA sql.NullBool + var forceMFALocalOnly sql.NullBool + err := row.Scan( + &userType, + &forceMFA, + &forceMFALocalOnly, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return zerrors.ThrowNotFound(err, "QUERY-SF3h2", "Errors.Internal") + } + return zerrors.ThrowInternal(err, "QUERY-Sf3rt", "Errors.Internal") + } + requirements = &UserAuthMethodRequirements{ + UserType: domain.UserType(userType.Int32), + ForceMFA: forceMFA.Bool, + ForceMFALocalOnly: forceMFALocalOnly.Bool, + } + return nil + }, + listUserAuthMethodTypesStmt, + userID, + authz.GetInstance(ctx).InstanceID(), + ) if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal") } @@ -349,7 +380,7 @@ func (q *UserAuthMethodSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectB return query } -func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethods, error)) { +func prepareUserAuthMethodsQuery() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethods, error)) { return sq.Select( UserAuthMethodColumnTokenID.identifier(), UserAuthMethodColumnCreationDate.identifier(), @@ -361,7 +392,7 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se UserAuthMethodColumnState.identifier(), UserAuthMethodColumnMethodType.identifier(), countColumn.identifier()). - From(userAuthMethodTable.identifier() + db.Timetravel(call.Took(ctx))). + From(userAuthMethodTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*AuthMethods, error) { userAuthMethods := make([]*AuthMethod, 0) @@ -399,7 +430,7 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase, activeOnly bool, includeWithoutDomain bool, queryDomain string) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { +func prepareUserAuthMethodTypesQuery(activeOnly bool, includeWithoutDomain bool, queryDomain string) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery(activeOnly, includeWithoutDomain, queryDomain) if err != nil { return sq.SelectBuilder{}, nil @@ -420,7 +451,7 @@ func prepareUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase, ac authMethodsArgs...). LeftJoin("(" + idpsQuery + ") AS " + userIDPsCountTable.alias + " ON " + userIDPsCountUserID.identifier() + " = " + UserIDCol.identifier() + " AND " + - userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))). + userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*AuthMethodTypes, error) { userAuthMethodTypes := make([]domain.UserAuthMethodType, 0) @@ -461,45 +492,6 @@ func prepareUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase, ac } } -func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - loginPolicyQuery, err := prepareAuthMethodsForceMFAQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - return sq.Select( - UserTypeCol.identifier(), - forceMFAForce.identifier(), - forceMFAForceLocalOnly.identifier()). - From(userTable.identifier()). - LeftJoin("(" + loginPolicyQuery + ") AS " + forceMFATable.alias + " ON " + - "(" + forceMFAOrgID.identifier() + " = " + UserInstanceIDCol.identifier() + " OR " + forceMFAOrgID.identifier() + " = " + UserResourceOwnerCol.identifier() + ") AND " + - forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()). - OrderBy(forceMFAIsDefault.identifier()). - Limit(1). - PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*UserAuthMethodRequirements, error) { - var userType sql.NullInt32 - var forceMFA sql.NullBool - var forceMFALocalOnly sql.NullBool - err := row.Scan( - &userType, - &forceMFA, - &forceMFALocalOnly, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-SF3h2", "Errors.Internal") - } - return nil, zerrors.ThrowInternal(err, "QUERY-Sf3rt", "Errors.Internal") - } - return &UserAuthMethodRequirements{ - UserType: domain.UserType(userType.Int32), - ForceMFA: forceMFA.Bool, - ForceMFALocalOnly: forceMFALocalOnly.Bool, - }, nil - } -} - func prepareAuthMethodsIDPsQuery() (string, error) { idpsQuery, _, err := sq.Select( userIDPsCountUserID.identifier(), @@ -536,16 +528,3 @@ func prepareAuthMethodQuery(activeOnly bool, includeWithoutDomain bool, queryDom return q.ToSql() } - -func prepareAuthMethodsForceMFAQuery() (string, error) { - loginPolicyQuery, _, err := sq.Select( - forceMFAForce.identifier(), - forceMFAForceLocalOnly.identifier(), - forceMFAInstanceID.identifier(), - forceMFAOrgID.identifier(), - forceMFAIsDefault.identifier(), - ). - From(forceMFATable.identifier()). - ToSql() - return loginPolicyQuery, err -} diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index 041e4f8e9e..03f2e2174a 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -14,7 +14,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/zerrors" ) func TestUser_authMethodsCheckPermission(t *testing.T) { @@ -191,8 +190,7 @@ var ( ` projections.user_auth_methods5.state,` + ` projections.user_auth_methods5.method_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.user_auth_methods5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.user_auth_methods5` prepareUserAuthMethodsCols = []string{ "token_id", "creation_date", @@ -215,8 +213,7 @@ var ( ` ON auth_method_types.user_id = projections.users14.id AND auth_method_types.instance_id = projections.users14.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms` + ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` prepareActiveAuthMethodTypesCols = []string{ "password_set", "method_type", @@ -232,8 +229,7 @@ var ( ` ON auth_method_types.user_id = projections.users14.id AND auth_method_types.instance_id = projections.users14.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms` + ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` prepareActiveAuthMethodTypesDomainCols = []string{ "password_set", "method_type", @@ -249,8 +245,7 @@ var ( ` ON auth_method_types.user_id = projections.users14.id AND auth_method_types.instance_id = projections.users14.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms` + ` ON user_idps_count.user_id = projections.users14.id AND user_idps_count.instance_id = projections.users14.instance_id` prepareActiveAuthMethodTypesDomainExternalCols = []string{ "password_set", "method_type", @@ -417,8 +412,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -434,8 +429,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery one second factor", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -466,8 +461,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery one second factor with domain", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "example.com") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "example.com") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -498,8 +493,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery one second factor with domain external", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, false, "example.com") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, false, "example.com") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -530,8 +525,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery multiple second factors", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -568,8 +563,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery multiple second factors domain", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "example.com") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "example.com") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -606,8 +601,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery multiple second factors domain external", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, false, "example.com") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, false, "example.com") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -644,8 +639,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") + prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -664,110 +659,10 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, object: (*AuthMethodTypes)(nil), }, - { - name: "prepareUserAuthMethodTypesRequiredQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) { - return scan(row) - } - }, - want: want{ - sqlExpectations: mockQueriesScanErr( - regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: (*UserAuthMethodRequirements)(nil), - }, - { - name: "prepareUserAuthMethodTypesRequiredQuery one second factor", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) { - return scan(row) - } - }, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt), - prepareAuthMethodTypesRequiredCols, - [][]driver.Value{ - { - domain.UserTypeHuman, - true, - true, - }, - }, - ), - }, - object: &UserAuthMethodRequirements{ - UserType: domain.UserTypeHuman, - ForceMFA: true, - ForceMFALocalOnly: true, - }, - }, - { - name: "prepareUserAuthMethodTypesRequiredQuery multiple second factors", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) { - return scan(row) - } - }, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt), - prepareAuthMethodTypesRequiredCols, - [][]driver.Value{ - { - domain.UserTypeHuman, - true, - true, - }, - }, - ), - }, - - object: &UserAuthMethodRequirements{ - UserType: domain.UserTypeHuman, - ForceMFA: true, - ForceMFALocalOnly: true, - }, - }, - { - name: "prepareUserAuthMethodTypesRequiredQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) { - builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) { - return scan(row) - } - }, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt), - sql.ErrConnDone, - ), - err: func(err error) (error, bool) { - if !errors.Is(err, sql.ErrConnDone) { - return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false - } - return nil, true - }, - }, - object: nil, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_auth_method_types_required.sql b/internal/query/user_auth_method_types_required.sql new file mode 100644 index 0000000000..d10420f0eb --- /dev/null +++ b/internal/query/user_auth_method_types_required.sql @@ -0,0 +1,17 @@ +SELECT + projections.users14.type + , auth_methods_force_mfa.force_mfa + , auth_methods_force_mfa.force_mfa_local_only +FROM + projections.users14 +LEFT JOIN + projections.login_policies5 AS auth_methods_force_mfa +ON + auth_methods_force_mfa.instance_id = projections.users14.instance_id + AND auth_methods_force_mfa.aggregate_id = ANY(ARRAY[projections.users14.instance_id, projections.users14.resource_owner]) +WHERE + projections.users14.id = $1 + AND projections.users14.instance_id = $2 +ORDER BY + auth_methods_force_mfa.is_default +LIMIT 1; \ No newline at end of file diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 265d8eaae1..c3f24c066e 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -246,7 +245,7 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, queries traceSpan.EndWithError(err) } - query, scan := prepareUserGrantQuery(ctx, q.client) + query, scan := prepareUserGrantQuery() for _, q := range queries { query = q.toQuery(query) } @@ -274,7 +273,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh traceSpan.EndWithError(err) } - query, scan := prepareUserGrantsQuery(ctx, q.client) + query, scan := prepareUserGrantsQuery() eq := sq.Eq{UserGrantInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -298,7 +297,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return grants, nil } -func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserGrant, error)) { +func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, error)) { return sq.Select( UserGrantID.identifier(), UserGrantCreationDate.identifier(), @@ -336,7 +335,7 @@ func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). - LeftJoin(join(LoginNameUserIDCol, UserGrantUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), @@ -421,7 +420,7 @@ func prepareUserGrantQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } } -func prepareUserGrantsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, error)) { +func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, error)) { return sq.Select( UserGrantID.identifier(), UserGrantCreationDate.identifier(), @@ -461,7 +460,7 @@ func prepareUserGrantsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). - LeftJoin(join(LoginNameUserIDCol, UserGrantUserID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 6cfa0b563b..6a640c2ef2 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -47,7 +47,6 @@ var ( " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + - ` AS OF SYSTEM TIME '-1 ms' ` + " WHERE projections.login_names3.is_primary = $1") userGrantCols = []string{ "id", @@ -110,7 +109,6 @@ var ( " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + - ` AS OF SYSTEM TIME '-1 ms' ` + " WHERE projections.login_names3.is_primary = $1") userGrantsCols = append( userGrantCols, @@ -1008,7 +1006,7 @@ func Test_UserGrantPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index 7ba2629cfa..cae2b4dae3 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -9,7 +9,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -138,7 +137,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer wg.Wait() } - query, queryArgs, scan := prepareMembershipsQuery(ctx, q.client, queries) + query, queryArgs, scan := prepareMembershipsQuery(queries) eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -237,7 +236,7 @@ func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface args } -func prepareMembershipsQuery(ctx context.Context, db prepareDatabase, queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { +func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { query, args := getMembershipFromQuery(queries) return sq.Select( membershipUserID.identifier(), @@ -259,7 +258,7 @@ func prepareMembershipsQuery(ctx context.Context, db prepareDatabase, queries *M LeftJoin(join(ProjectColumnID, membershipProjectID)). LeftJoin(join(OrgColumnID, membershipOrgID)). LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)). - LeftJoin(join(InstanceColumnID, membershipInstanceID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(InstanceColumnID, membershipInstanceID)). PlaceholderFormat(sq.Dollar), args, func(rows *sql.Rows) (*Memberships, error) { diff --git a/internal/query/user_membership_test.go b/internal/query/user_membership_test.go index a0ea3cda31..b0170182d1 100644 --- a/internal/query/user_membership_test.go +++ b/internal/query/user_membership_test.go @@ -1,7 +1,6 @@ package query import ( - "context" "database/sql" "database/sql/driver" "errors" @@ -87,8 +86,7 @@ var ( " LEFT JOIN projections.projects4 ON members.project_id = projections.projects4.id AND members.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 ON members.org_id = projections.orgs1.id AND members.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id" + - " LEFT JOIN projections.instances ON members.instance_id = projections.instances.id" + - ` AS OF SYSTEM TIME '-1 ms'`) + " LEFT JOIN projections.instances ON members.instance_id = projections.instances.id") membershipCols = []string{ "user_id", "roles", @@ -456,14 +454,14 @@ func Test_MembershipPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } -func prepareMembershipWrapper() func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { - return func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { - builder, _, fun := prepareMembershipsQuery(ctx, db, &MembershipSearchQuery{}) +func prepareMembershipWrapper() func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { + return func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { + builder, _, fun := prepareMembershipsQuery(&MembershipSearchQuery{}) return builder, fun } } diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go index a3b7c1fd34..ff612f82c8 100644 --- a/internal/query/user_metadata.go +++ b/internal/query/user_metadata.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -87,7 +86,7 @@ func (q *Queries) GetUserMetadataByKey(ctx context.Context, shouldTriggerBulk bo traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataQuery(ctx, q.client) + query, scan := prepareUserMetadataQuery() for _, q := range queries { query = q.toQuery(query) } @@ -119,7 +118,7 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataListQuery(ctx, q.client) + query, scan := prepareUserMetadataListQuery() eq := sq.Eq{ UserMetadataUserIDCol.identifier(): userIDs, UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -151,7 +150,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool traceSpan.EndWithError(err) } - query, scan := prepareUserMetadataListQuery(ctx, q.client) + query, scan := prepareUserMetadataListQuery() eq := sq.Eq{ UserMetadataUserIDCol.identifier(): userID, UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -235,7 +234,7 @@ func NewUserMetadataExistsQuery(key string, value []byte, keyComparison TextComp ) } -func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserMetadata, error)) { +func prepareUserMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*UserMetadata, error)) { return sq.Select( UserMetadataCreationDateCol.identifier(), UserMetadataChangeDateCol.identifier(), @@ -244,7 +243,7 @@ func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Selec UserMetadataKeyCol.identifier(), UserMetadataValueCol.identifier(), ). - From(userMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(userMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*UserMetadata, error) { m := new(UserMetadata) @@ -267,7 +266,7 @@ func prepareUserMetadataQuery(ctx context.Context, db prepareDatabase) (sq.Selec } } -func prepareUserMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserMetadataList, error)) { +func prepareUserMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserMetadataList, error)) { return sq.Select( UserMetadataCreationDateCol.identifier(), UserMetadataChangeDateCol.identifier(), @@ -277,7 +276,7 @@ func prepareUserMetadataListQuery(ctx context.Context, db prepareDatabase) (sq.S UserMetadataKeyCol.identifier(), UserMetadataValueCol.identifier(), countColumn.identifier()). - From(userMetadataTable.identifier() + db.Timetravel(call.Took(ctx))). + From(userMetadataTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*UserMetadataList, error) { metadata := make([]*UserMetadata, 0) diff --git a/internal/query/user_metadata_test.go b/internal/query/user_metadata_test.go index 7f9d1b8ed3..6236272da4 100644 --- a/internal/query/user_metadata_test.go +++ b/internal/query/user_metadata_test.go @@ -18,8 +18,7 @@ var ( ` projections.user_metadata5.sequence,` + ` projections.user_metadata5.key,` + ` projections.user_metadata5.value` + - ` FROM projections.user_metadata5` + - ` AS OF SYSTEM TIME '-1 ms'` + ` FROM projections.user_metadata5` userMetadataCols = []string{ "creation_date", "change_date", @@ -251,7 +250,7 @@ func Test_UserMetadataPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_password.go b/internal/query/user_password.go index ed77d0d3ae..1d0037f721 100644 --- a/internal/query/user_password.go +++ b/internal/query/user_password.go @@ -119,7 +119,6 @@ func (wm *HumanPasswordReadModel) Reduce() error { func (wm *HumanPasswordReadModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AwaitOpenTransactions(). - AllowTimeTravel(). AddQuery(). AggregateTypes(user.AggregateType). AggregateIDs(wm.AggregateID). diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index dadd635b6a..8ea33f51a4 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" @@ -98,7 +97,7 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk traceSpan.EndWithError(err) } - query, scan := preparePersonalAccessTokenQuery(ctx, q.client) + query, scan := preparePersonalAccessTokenQuery() for _, q := range queries { query = q.toQuery(query) } @@ -128,7 +127,7 @@ func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *Perso ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := preparePersonalAccessTokensQuery(ctx, q.client) + query, scan := preparePersonalAccessTokensQuery() eq := sq.Eq{ PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -178,7 +177,7 @@ func (q *PersonalAccessTokenSearchQueries) toQuery(query sq.SelectBuilder) sq.Se return query } -func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*PersonalAccessToken, error)) { +func preparePersonalAccessTokenQuery() (sq.SelectBuilder, func(*sql.Row) (*PersonalAccessToken, error)) { return sq.Select( PersonalAccessTokenColumnID.identifier(), PersonalAccessTokenColumnCreationDate.identifier(), @@ -188,7 +187,7 @@ func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (s PersonalAccessTokenColumnUserID.identifier(), PersonalAccessTokenColumnExpiration.identifier(), PersonalAccessTokenColumnScopes.identifier()). - From(personalAccessTokensTable.identifier() + db.Timetravel(call.Took(ctx))). + From(personalAccessTokensTable.identifier()). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*PersonalAccessToken, error) { p := new(PersonalAccessToken) @@ -212,7 +211,7 @@ func preparePersonalAccessTokenQuery(ctx context.Context, db prepareDatabase) (s } } -func preparePersonalAccessTokensQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*PersonalAccessTokens, error)) { +func preparePersonalAccessTokensQuery() (sq.SelectBuilder, func(*sql.Rows) (*PersonalAccessTokens, error)) { return sq.Select( PersonalAccessTokenColumnID.identifier(), PersonalAccessTokenColumnCreationDate.identifier(), @@ -223,7 +222,7 @@ func preparePersonalAccessTokensQuery(ctx context.Context, db prepareDatabase) ( PersonalAccessTokenColumnExpiration.identifier(), PersonalAccessTokenColumnScopes.identifier(), countColumn.identifier()). - From(personalAccessTokensTable.identifier() + db.Timetravel(call.Took(ctx))). + From(personalAccessTokensTable.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*PersonalAccessTokens, error) { personalAccessTokens := make([]*PersonalAccessToken, 0) diff --git a/internal/query/user_personal_access_token_test.go b/internal/query/user_personal_access_token_test.go index 79ba700ed5..dd3ed37e62 100644 --- a/internal/query/user_personal_access_token_test.go +++ b/internal/query/user_personal_access_token_test.go @@ -23,8 +23,7 @@ var ( " projections.personal_access_tokens3.user_id," + " projections.personal_access_tokens3.expiration," + " projections.personal_access_tokens3.scopes" + - " FROM projections.personal_access_tokens3" + - ` AS OF SYSTEM TIME '-1 ms'`) + " FROM projections.personal_access_tokens3") personalAccessTokenCols = []string{ "id", "creation_date", @@ -45,8 +44,7 @@ var ( " projections.personal_access_tokens3.expiration," + " projections.personal_access_tokens3.scopes," + " COUNT(*) OVER ()" + - " FROM projections.personal_access_tokens3" + - " AS OF SYSTEM TIME '-1 ms'") + " FROM projections.personal_access_tokens3") personalAccessTokensCols = []string{ "id", "creation_date", @@ -266,7 +264,7 @@ func Test_PersonalAccessTokenPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/user_schema.go b/internal/query/user_schema.go index ff5117d264..d3ea4dee79 100644 --- a/internal/query/user_schema.go +++ b/internal/query/user_schema.go @@ -96,7 +96,7 @@ func (q *Queries) GetUserSchemaByID(ctx context.Context, id string) (userSchema } query, scan := prepareUserSchemaQuery() - return genericRowQuery[*UserSchema](ctx, q.client, query.Where(eq), scan) + return genericRowQuery(ctx, q.client, query.Where(eq), scan) } func (q *Queries) SearchUserSchema(ctx context.Context, queries *UserSchemaSearchQueries) (userSchemas *UserSchemas, err error) { @@ -108,7 +108,7 @@ func (q *Queries) SearchUserSchema(ctx context.Context, queries *UserSchemaSearc } query, scan := prepareUserSchemasQuery() - return genericRowsQueryWithState[*UserSchemas](ctx, q.client, userSchemaTable, combineToWhereStmt(query, queries.toQuery, eq), scan) + return genericRowsQueryWithState(ctx, q.client, userSchemaTable, combineToWhereStmt(query, queries.toQuery, eq), scan) } func (q *UserSchemaSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 16b08611f0..50d65cc1ec 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -6,7 +6,6 @@ import ( "database/sql/driver" "errors" "fmt" - "reflect" "regexp" "testing" @@ -268,8 +267,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` userCols = []string{ "id", "creation_date", @@ -319,8 +317,7 @@ var ( ` projections.users14_humans.gender,` + ` projections.users14_humans.avatar_key` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` profileCols = []string{ "id", "creation_date", @@ -345,8 +342,7 @@ var ( ` projections.users14_humans.email,` + ` projections.users14_humans.is_email_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` emailCols = []string{ "id", "creation_date", @@ -366,8 +362,7 @@ var ( ` projections.users14_humans.phone,` + ` projections.users14_humans.is_phone_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` phoneCols = []string{ "id", "creation_date", @@ -385,8 +380,7 @@ var ( ` projections.users14_humans.email,` + ` projections.users14_humans.is_email_verified` + ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` userUniqueCols = []string{ "id", "state", @@ -428,8 +422,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` notifyUserCols = []string{ "id", "creation_date", @@ -497,8 +490,7 @@ var ( ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'` + ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` usersCols = []string{ "id", "creation_date", @@ -1572,12 +1564,7 @@ func Test_UserPrepares(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - params := defaultPrepareArgs - if reflect.TypeOf(tt.prepare).NumIn() == 0 { - params = []reflect.Value{} - } - - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, params...) + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) }) } } diff --git a/internal/query/userinfo_test.go b/internal/query/userinfo_test.go index 6ded7b4eed..5314283635 100644 --- a/internal/query/userinfo_test.go +++ b/internal/query/userinfo_test.go @@ -429,8 +429,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") @@ -476,8 +475,7 @@ func TestQueries_GetOIDCUserinfoClientByID(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") diff --git a/internal/query/web_key_test.go b/internal/query/web_key_test.go index 6008ec6528..80d07bfa13 100644 --- a/internal/query/web_key_test.go +++ b/internal/query/web_key_test.go @@ -208,8 +208,7 @@ func TestQueries_GetActiveSigningWebKey(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, keyEncryptionAlgorithm: alg, } @@ -307,8 +306,7 @@ func TestQueries_ListWebKeys(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } got, err := q.ListWebKeys(ctx) @@ -369,8 +367,7 @@ func TestQueries_GetWebKeySet(t *testing.T) { execMock(t, tt.mock, func(db *sql.DB) { q := &Queries{ client: &database.DB{ - DB: db, - Database: &prepareDB{}, + DB: db, }, } got, err := q.GetWebKeySet(ctx) diff --git a/internal/queue/database.go b/internal/queue/database.go new file mode 100644 index 0000000000..c5eb0b8ca3 --- /dev/null +++ b/internal/queue/database.go @@ -0,0 +1,45 @@ +package queue + +import ( + "context" + "sync" + + "github.com/jackc/pgx/v5" + + "github.com/zitadel/zitadel/internal/database/dialect" +) + +const ( + schema = "queue" + applicationName = "zitadel_queue" +) + +var conns = &sync.Map{} + +type queueKey struct{} + +func WithQueue(parent context.Context) context.Context { + return context.WithValue(parent, queueKey{}, struct{}{}) +} + +func init() { + dialect.RegisterBeforeAcquire(func(ctx context.Context, c *pgx.Conn) error { + if _, ok := ctx.Value(queueKey{}).(struct{}); !ok { + return nil + } + _, err := c.Exec(ctx, "SET search_path TO "+schema+"; SET application_name TO "+applicationName) + if err != nil { + return err + } + conns.Store(c, struct{}{}) + return nil + }) + dialect.RegisterAfterRelease(func(c *pgx.Conn) error { + _, ok := conns.LoadAndDelete(c) + if !ok { + return nil + } + _, err := c.Exec(context.Background(), "SET search_path TO DEFAULT; SET application_name TO "+dialect.DefaultAppName) + return err + }) +} diff --git a/internal/queue/migrate.go b/internal/queue/migrate.go new file mode 100644 index 0000000000..e814da3bd3 --- /dev/null +++ b/internal/queue/migrate.go @@ -0,0 +1,38 @@ +package queue + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/riverqueue/river/riverdriver" + "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivermigrate" + + "github.com/zitadel/zitadel/internal/database" +) + +type Migrator struct { + driver riverdriver.Driver[pgx.Tx] +} + +func NewMigrator(client *database.DB) *Migrator { + return &Migrator{ + driver: riverpgxv5.New(client.Pool), + } +} + +func (m *Migrator) Execute(ctx context.Context) error { + _, err := m.driver.GetExecutor().Exec(ctx, "CREATE SCHEMA IF NOT EXISTS "+schema) + if err != nil { + return err + } + + migrator, err := rivermigrate.New(m.driver, nil) + if err != nil { + return err + } + ctx = WithQueue(ctx) + _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, nil) + return err + +} diff --git a/internal/queue/queue.go b/internal/queue/queue.go index 265988e9ef..d680221753 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -2,74 +2,96 @@ package queue import ( "context" - "sync" "github.com/jackc/pgx/v5" + "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver" "github.com/riverqueue/river/riverdriver/riverpgxv5" - "github.com/riverqueue/river/rivermigrate" + "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/dialect" ) -const ( - schema = "queue" - applicationName = "zitadel_queue" -) - -var conns = &sync.Map{} - -type queueKey struct{} - -func WithQueue(parent context.Context) context.Context { - return context.WithValue(parent, queueKey{}, struct{}{}) -} - -func init() { - dialect.RegisterBeforeAcquire(func(ctx context.Context, c *pgx.Conn) error { - if _, ok := ctx.Value(queueKey{}).(struct{}); !ok { - return nil - } - _, err := c.Exec(ctx, "SET search_path TO "+schema+"; SET application_name TO "+applicationName) - if err != nil { - return err - } - conns.Store(c, struct{}{}) - return nil - }) - dialect.RegisterAfterRelease(func(c *pgx.Conn) error { - _, ok := conns.LoadAndDelete(c) - if !ok { - return nil - } - _, err := c.Exec(context.Background(), "SET search_path TO DEFAULT; SET application_name TO "+dialect.DefaultAppName) - return err - }) -} - // Queue abstracts the underlying queuing library // For more information see github.com/riverqueue/river -// TODO(adlerhurst): maybe it makes more sense to split the effective queue from the migrator. type Queue struct { driver riverdriver.Driver[pgx.Tx] + client *river.Client[pgx.Tx] + + config *river.Config + shouldStart bool } -func New(client *database.DB) *Queue { - return &Queue{driver: riverpgxv5.New(client.Pool)} +type Config struct { + Client *database.DB `mapstructure:"-"` // mapstructure is needed if we would like to use viper to configure the queue } -func (q *Queue) ExecuteMigrations(ctx context.Context) error { - _, err := q.driver.GetExecutor().Exec(ctx, "CREATE SCHEMA IF NOT EXISTS "+schema) - if err != nil { - return err +func NewQueue(config *Config) (_ *Queue, err error) { + return &Queue{ + driver: riverpgxv5.New(config.Client.Pool), + config: &river.Config{ + Workers: river.NewWorkers(), + Queues: make(map[string]river.QueueConfig), + JobTimeout: -1, + }, + }, nil +} + +func (q *Queue) ShouldStart() { + if q == nil { + return } + q.shouldStart = true +} - migrator, err := rivermigrate.New(q.driver, nil) - if err != nil { - return err +func (q *Queue) Start(ctx context.Context) (err error) { + if q == nil || !q.shouldStart { + return nil } ctx = WithQueue(ctx) - _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, nil) + + q.client, err = river.NewClient(q.driver, q.config) + if err != nil { + return err + } + + return q.client.Start(ctx) +} + +func (q *Queue) AddWorkers(w ...Worker) { + if q == nil { + logging.Info("skip adding workers because queue is not set") + return + } + for _, worker := range w { + worker.Register(q.config.Workers, q.config.Queues) + } +} + +type InsertOpt func(*river.InsertOpts) + +func WithMaxAttempts(maxAttempts uint8) InsertOpt { + return func(opts *river.InsertOpts) { + opts.MaxAttempts = int(maxAttempts) + } +} + +func WithQueueName(name string) InsertOpt { + return func(opts *river.InsertOpts) { + opts.Queue = name + } +} + +func (q *Queue) Insert(ctx context.Context, args river.JobArgs, opts ...InsertOpt) error { + options := new(river.InsertOpts) + ctx = WithQueue(ctx) + for _, opt := range opts { + opt(options) + } + _, err := q.client.Insert(ctx, args, options) return err } + +type Worker interface { + Register(workers *river.Workers, queues map[string]river.QueueConfig) +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 42ccc98936..ada6a2191a 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -3,7 +3,7 @@ package renderer import ( "context" "html/template" - "io/ioutil" + "io" "net/http" "os" @@ -107,7 +107,7 @@ func (r *Renderer) addFileToTemplate(dir http.FileSystem, tmpl *template.Templat return err } defer f.Close() - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return err } diff --git a/internal/repository/authrequest/auth_request.go b/internal/repository/authrequest/auth_request.go index 99f034333b..75624e3a21 100644 --- a/internal/repository/authrequest/auth_request.go +++ b/internal/repository/authrequest/auth_request.go @@ -38,6 +38,7 @@ type AddedEvent struct { LoginHint *string `json:"login_hint,omitempty"` HintUserID *string `json:"hint_user_id,omitempty"` NeedRefreshToken bool `json:"need_refresh_token,omitempty"` + Issuer string `json:"issuer,omitempty"` } func (e *AddedEvent) Payload() interface{} { @@ -66,6 +67,7 @@ func NewAddedEvent(ctx context.Context, loginHint, hintUserID *string, needRefreshToken bool, + issuer string, ) *AddedEvent { return &AddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -89,6 +91,7 @@ func NewAddedEvent(ctx context.Context, LoginHint: loginHint, HintUserID: hintUserID, NeedRefreshToken: needRefreshToken, + Issuer: issuer, } } diff --git a/internal/repository/execution/queue.go b/internal/repository/execution/queue.go new file mode 100644 index 0000000000..ed3a6ce4a0 --- /dev/null +++ b/internal/repository/execution/queue.go @@ -0,0 +1,71 @@ +package execution + +import ( + "encoding/json" + "time" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + QueueName = "execution" +) + +type Request struct { + Aggregate *eventstore.Aggregate `json:"aggregate"` + Sequence uint64 `json:"sequence"` + EventType eventstore.EventType `json:"eventType"` + CreatedAt time.Time `json:"createdAt"` + UserID string `json:"userID"` + EventData []byte `json:"eventData"` + TargetsData []byte `json:"targetsData"` +} + +func (e *Request) Kind() string { + return "execution_request" +} + +func ContextInfoFromRequest(e *Request) *ContextInfoEvent { + return &ContextInfoEvent{ + AggregateID: e.Aggregate.ID, + AggregateType: string(e.Aggregate.Type), + ResourceOwner: e.Aggregate.ResourceOwner, + InstanceID: e.Aggregate.InstanceID, + Version: string(e.Aggregate.Version), + Sequence: e.Sequence, + EventType: string(e.EventType), + CreatedAt: e.CreatedAt.Format(time.RFC3339Nano), + UserID: e.UserID, + EventPayload: e.EventData, + } +} + +type ContextInfoEvent struct { + AggregateID string `json:"aggregateID,omitempty"` + AggregateType string `json:"aggregateType,omitempty"` + ResourceOwner string `json:"resourceOwner,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + Version string `json:"version,omitempty"` + Sequence uint64 `json:"sequence,omitempty"` + EventType string `json:"event_type,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UserID string `json:"userID,omitempty"` + EventPayload json.RawMessage `json:"event_payload,omitempty"` +} + +func (c *ContextInfoEvent) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfoEvent) SetHTTPResponseBody(resp []byte) error { + // response is irrelevant and will not be unmarshaled + return nil +} + +func (c *ContextInfoEvent) GetContent() any { + return c.EventPayload +} diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go index f5e033af1c..00618f56c2 100644 --- a/internal/repository/feature/feature_v2/eventstore.go +++ b/internal/repository/feature/feature_v2/eventstore.go @@ -12,7 +12,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SystemLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, SystemActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]]) @@ -26,7 +25,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, InstanceActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceDebugOIDCParentErrorEventType, eventstore.GenericEventMapper[SetEvent[bool]]) @@ -35,4 +33,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceEnableBackChannelLogout, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginVersion, eventstore.GenericEventMapper[SetEvent[*feature.LoginV2]]) eventstore.RegisterFilterEventMapper(AggregateType, InstancePermissionCheckV2, eventstore.GenericEventMapper[SetEvent[bool]]) + eventstore.RegisterFilterEventMapper(AggregateType, InstanceConsoleUseV2UserApi, eventstore.GenericEventMapper[SetEvent[bool]]) } diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go index 331a5143f9..d5e8941df2 100644 --- a/internal/repository/feature/feature_v2/feature.go +++ b/internal/repository/feature/feature_v2/feature.go @@ -17,7 +17,6 @@ var ( SystemLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLegacyIntrospection) SystemUserSchemaEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyUserSchema) SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange) - SystemActionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyActions) SystemImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyImprovedPerformance) SystemOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyOIDCSingleV1SessionTermination) SystemDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelSystem, feature.KeyDisableUserTokenEvent) @@ -31,7 +30,6 @@ var ( InstanceLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLegacyIntrospection) InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema) InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange) - InstanceActionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyActions) InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey) InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError) @@ -40,6 +38,7 @@ var ( InstanceEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelInstance, feature.KeyEnableBackChannelLogout) InstanceLoginVersion = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginV2) InstancePermissionCheckV2 = setEventTypeFromFeature(feature.LevelInstance, feature.KeyPermissionCheckV2) + InstanceConsoleUseV2UserApi = setEventTypeFromFeature(feature.LevelInstance, feature.KeyConsoleUseV2UserApi) ) const ( diff --git a/internal/repository/idp/ldap.go b/internal/repository/idp/ldap.go index 5f5bb9ced5..56fb18c5b4 100644 --- a/internal/repository/idp/ldap.go +++ b/internal/repository/idp/ldap.go @@ -22,6 +22,7 @@ type LDAPIDPAddedEvent struct { UserObjectClasses []string `json:"userObjectClasses"` UserFilters []string `json:"userFilters"` Timeout time.Duration `json:"timeout"` + RootCA []byte `json:"rootCA"` LDAPAttributes Options @@ -142,6 +143,7 @@ func NewLDAPIDPAddedEvent( userObjectClasses []string, userFilters []string, timeout time.Duration, + rootCA []byte, attributes LDAPAttributes, options Options, ) *LDAPIDPAddedEvent { @@ -158,6 +160,7 @@ func NewLDAPIDPAddedEvent( UserObjectClasses: userObjectClasses, UserFilters: userFilters, Timeout: timeout, + RootCA: rootCA, LDAPAttributes: attributes, Options: options, } @@ -198,6 +201,7 @@ type LDAPIDPChangedEvent struct { UserObjectClasses []string `json:"userObjectClasses,omitempty"` UserFilters []string `json:"userFilters,omitempty"` Timeout *time.Duration `json:"timeout,omitempty"` + RootCA []byte `json:"rootCA,omitempty"` LDAPAttributeChanges OptionChanges @@ -315,6 +319,12 @@ func ChangeLDAPTimeout(timeout time.Duration) func(*LDAPIDPChangedEvent) { } } +func ChangeLDAPRootCA(rootCA []byte) func(*LDAPIDPChangedEvent) { + return func(e *LDAPIDPChangedEvent) { + e.RootCA = rootCA + } +} + func ChangeLDAPAttributes(attributes LDAPAttributeChanges) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { e.LDAPAttributeChanges = attributes diff --git a/internal/repository/idp/oauth.go b/internal/repository/idp/oauth.go index 9b9b776082..e168459eea 100644 --- a/internal/repository/idp/oauth.go +++ b/internal/repository/idp/oauth.go @@ -18,6 +18,7 @@ type OAuthIDPAddedEvent struct { UserEndpoint string `json:"userEndpoint,omitempty"` Scopes []string `json:"scopes,omitempty"` IDAttribute string `json:"idAttribute,omitempty"` + UsePKCE bool `json:"usePKCE,omitempty"` Options } @@ -32,6 +33,7 @@ func NewOAuthIDPAddedEvent( userEndpoint, idAttribute string, scopes []string, + usePKCE bool, options Options, ) *OAuthIDPAddedEvent { return &OAuthIDPAddedEvent{ @@ -45,6 +47,7 @@ func NewOAuthIDPAddedEvent( UserEndpoint: userEndpoint, Scopes: scopes, IDAttribute: idAttribute, + UsePKCE: usePKCE, Options: options, } } @@ -82,6 +85,7 @@ type OAuthIDPChangedEvent struct { UserEndpoint *string `json:"userEndpoint,omitempty"` Scopes []string `json:"scopes,omitempty"` IDAttribute *string `json:"idAttribute,omitempty"` + UsePKCE *bool `json:"usePKCE,omitempty"` OptionChanges } @@ -158,6 +162,12 @@ func ChangeOAuthIDAttribute(idAttribute string) func(*OAuthIDPChangedEvent) { } } +func ChangeOAuthUsePKCE(usePKCE bool) func(*OAuthIDPChangedEvent) { + return func(e *OAuthIDPChangedEvent) { + e.UsePKCE = &usePKCE + } +} + func (e *OAuthIDPChangedEvent) Payload() interface{} { return e } diff --git a/internal/repository/idp/oidc.go b/internal/repository/idp/oidc.go index 0970129ceb..8c51baa6cf 100644 --- a/internal/repository/idp/oidc.go +++ b/internal/repository/idp/oidc.go @@ -16,6 +16,7 @@ type OIDCIDPAddedEvent struct { ClientSecret *crypto.CryptoValue `json:"clientSecret"` Scopes []string `json:"scopes,omitempty"` IsIDTokenMapping bool `json:"idTokenMapping,omitempty"` + UsePKCE bool `json:"usePKCE,omitempty"` Options } @@ -27,7 +28,7 @@ func NewOIDCIDPAddedEvent( clientID string, clientSecret *crypto.CryptoValue, scopes []string, - isIDTokenMapping bool, + isIDTokenMapping, usePKCE bool, options Options, ) *OIDCIDPAddedEvent { return &OIDCIDPAddedEvent{ @@ -39,6 +40,7 @@ func NewOIDCIDPAddedEvent( ClientSecret: clientSecret, Scopes: scopes, IsIDTokenMapping: isIDTokenMapping, + UsePKCE: usePKCE, Options: options, } } @@ -74,6 +76,7 @@ type OIDCIDPChangedEvent struct { ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"` Scopes []string `json:"scopes,omitempty"` IsIDTokenMapping *bool `json:"idTokenMapping,omitempty"` + UsePKCE *bool `json:"usePKCE,omitempty"` OptionChanges } @@ -139,6 +142,12 @@ func ChangeOIDCIsIDTokenMapping(idTokenMapping bool) func(*OIDCIDPChangedEvent) } } +func ChangeOIDCUsePKCE(usePKCE bool) func(*OIDCIDPChangedEvent) { + return func(e *OIDCIDPChangedEvent) { + e.UsePKCE = &usePKCE + } +} + func (e *OIDCIDPChangedEvent) Payload() interface{} { return e } diff --git a/internal/repository/idpintent/intent.go b/internal/repository/idpintent/intent.go index 9ac1a875cc..27e6391f95 100644 --- a/internal/repository/idpintent/intent.go +++ b/internal/repository/idpintent/intent.go @@ -21,9 +21,10 @@ const ( type StartedEvent struct { eventstore.BaseEvent `json:"-"` - SuccessURL *url.URL `json:"successURL"` - FailureURL *url.URL `json:"failureURL"` - IDPID string `json:"idpId"` + SuccessURL *url.URL `json:"successURL"` + FailureURL *url.URL `json:"failureURL"` + IDPID string `json:"idpId"` + IDPArguments map[string]any `json:"idpArguments,omitempty"` } func NewStartedEvent( @@ -32,6 +33,7 @@ func NewStartedEvent( successURL, failureURL *url.URL, idpID string, + idpArguments map[string]any, ) *StartedEvent { return &StartedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -39,9 +41,10 @@ func NewStartedEvent( aggregate, StartedEventType, ), - SuccessURL: successURL, - FailureURL: failureURL, - IDPID: idpID, + SuccessURL: successURL, + FailureURL: failureURL, + IDPID: idpID, + IDPArguments: idpArguments, } } diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index d2f90ab4eb..6ab60c0dd5 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -56,6 +56,7 @@ func NewOAuthIDPAddedEvent( userEndpoint, idAttribute string, scopes []string, + usePKCE bool, options idp.Options, ) *OAuthIDPAddedEvent { @@ -75,6 +76,7 @@ func NewOAuthIDPAddedEvent( userEndpoint, idAttribute, scopes, + usePKCE, options, ), } @@ -137,7 +139,7 @@ func NewOIDCIDPAddedEvent( clientID string, clientSecret *crypto.CryptoValue, scopes []string, - isIDTokenMapping bool, + isIDTokenMapping, usePKCE bool, options idp.Options, ) *OIDCIDPAddedEvent { @@ -155,6 +157,7 @@ func NewOIDCIDPAddedEvent( clientSecret, scopes, isIDTokenMapping, + usePKCE, options, ), } @@ -852,6 +855,7 @@ func NewLDAPIDPAddedEvent( userObjectClasses []string, userFilters []string, timeout time.Duration, + rootCA []byte, attributes idp.LDAPAttributes, options idp.Options, ) *LDAPIDPAddedEvent { @@ -874,6 +878,7 @@ func NewLDAPIDPAddedEvent( userObjectClasses, userFilters, timeout, + rootCA, attributes, options, ), diff --git a/internal/repository/notification/aggregate.go b/internal/repository/notification/aggregate.go deleted file mode 100644 index 8370337d40..0000000000 --- a/internal/repository/notification/aggregate.go +++ /dev/null @@ -1,25 +0,0 @@ -package notification - -import ( - "github.com/zitadel/zitadel/internal/eventstore" -) - -const ( - AggregateType = "notification" - AggregateVersion = "v1" -) - -type Aggregate struct { - eventstore.Aggregate -} - -func NewAggregate(id, resourceOwner string) *Aggregate { - return &Aggregate{ - Aggregate: eventstore.Aggregate{ - Type: AggregateType, - Version: AggregateVersion, - ID: id, - ResourceOwner: resourceOwner, - }, - } -} diff --git a/internal/repository/notification/eventstore.go b/internal/repository/notification/eventstore.go deleted file mode 100644 index 3ef1c9c7db..0000000000 --- a/internal/repository/notification/eventstore.go +++ /dev/null @@ -1,12 +0,0 @@ -package notification - -import ( - "github.com/zitadel/zitadel/internal/eventstore" -) - -func init() { - eventstore.RegisterFilterEventMapper(AggregateType, RequestedType, eventstore.GenericEventMapper[RequestedEvent]) - eventstore.RegisterFilterEventMapper(AggregateType, SentType, eventstore.GenericEventMapper[SentEvent]) - eventstore.RegisterFilterEventMapper(AggregateType, RetryRequestedType, eventstore.GenericEventMapper[RetryRequestedEvent]) - eventstore.RegisterFilterEventMapper(AggregateType, CanceledType, eventstore.GenericEventMapper[CanceledEvent]) -} diff --git a/internal/repository/notification/notification.go b/internal/repository/notification/notification.go index cf7090525f..72e672f052 100644 --- a/internal/repository/notification/notification.go +++ b/internal/repository/notification/notification.go @@ -1,28 +1,21 @@ package notification import ( - "context" "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/query" ) const ( - notificationEventPrefix = "notification." - RequestedType = notificationEventPrefix + "requested" - RetryRequestedType = notificationEventPrefix + "retry.requested" - SentType = notificationEventPrefix + "sent" - CanceledType = notificationEventPrefix + "canceled" + QueueName = "notification" ) type Request struct { + Aggregate *eventstore.Aggregate `json:"aggregate"` UserID string `json:"userID"` UserResourceOwner string `json:"userResourceOwner"` - AggregateID string `json:"notificationAggregateID"` - AggregateResourceOwner string `json:"notificationAggregateResourceOwner"` TriggeredAtOrigin string `json:"triggeredAtOrigin"` EventType eventstore.EventType `json:"eventType"` MessageType string `json:"messageType"` @@ -32,213 +25,10 @@ type Request struct { Code *crypto.CryptoValue `json:"code,omitempty"` UnverifiedNotificationChannel bool `json:"unverifiedNotificationChannel,omitempty"` IsOTP bool `json:"isOTP,omitempty"` - RequiresPreviousDomain bool `json:"RequiresPreviousDomain,omitempty"` + RequiresPreviousDomain bool `json:"requiresPreviousDomain,omitempty"` Args *domain.NotificationArguments `json:"args,omitempty"` } -func (e *Request) NotificationAggregateID() string { - if e.AggregateID == "" { - return e.UserID - } - return e.AggregateID -} - -func (e *Request) NotificationAggregateResourceOwner() string { - if e.AggregateResourceOwner == "" { - return e.UserResourceOwner - } - return e.AggregateResourceOwner -} - -type RequestedEvent struct { - eventstore.BaseEvent `json:"-"` - - Request `json:"request"` -} - -func (e *RequestedEvent) TriggerOrigin() string { - return e.TriggeredAtOrigin -} - -func (e *RequestedEvent) Payload() interface{} { - return e -} - -func (e *RequestedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil -} - -func (e *RequestedEvent) SetBaseEvent(event *eventstore.BaseEvent) { - e.BaseEvent = *event -} - -func NewRequestedEvent(ctx context.Context, - aggregate *eventstore.Aggregate, - userID, - userResourceOwner, - aggregateID, - aggregateResourceOwner, - triggerOrigin, - urlTemplate string, - code *crypto.CryptoValue, - codeExpiry time.Duration, - eventType eventstore.EventType, - notificationType domain.NotificationType, - messageType string, - unverifiedNotificationChannel, - isOTP, - requiresPreviousDomain bool, - args *domain.NotificationArguments, -) *RequestedEvent { - return &RequestedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( - ctx, - aggregate, - RequestedType, - ), - Request: Request{ - UserID: userID, - UserResourceOwner: userResourceOwner, - AggregateID: aggregateID, - AggregateResourceOwner: aggregateResourceOwner, - TriggeredAtOrigin: triggerOrigin, - EventType: eventType, - MessageType: messageType, - NotificationType: notificationType, - URLTemplate: urlTemplate, - CodeExpiry: codeExpiry, - Code: code, - UnverifiedNotificationChannel: unverifiedNotificationChannel, - IsOTP: isOTP, - RequiresPreviousDomain: requiresPreviousDomain, - Args: args, - }, - } -} - -type SentEvent struct { - eventstore.BaseEvent `json:"-"` -} - -func (e *SentEvent) Payload() interface{} { - return e -} - -func (e *SentEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil -} - -func (e *SentEvent) SetBaseEvent(event *eventstore.BaseEvent) { - e.BaseEvent = *event -} - -func NewSentEvent(ctx context.Context, - aggregate *eventstore.Aggregate, -) *SentEvent { - return &SentEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( - ctx, - aggregate, - SentType, - ), - } -} - -type CanceledEvent struct { - eventstore.BaseEvent `json:"-"` - - Error string `json:"error"` -} - -func (e *CanceledEvent) Payload() interface{} { - return e -} - -func (e *CanceledEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil -} - -func (e *CanceledEvent) SetBaseEvent(event *eventstore.BaseEvent) { - e.BaseEvent = *event -} - -func NewCanceledEvent(ctx context.Context, aggregate *eventstore.Aggregate, errorMessage string) *CanceledEvent { - return &CanceledEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( - ctx, - aggregate, - CanceledType, - ), - Error: errorMessage, - } -} - -type RetryRequestedEvent struct { - eventstore.BaseEvent `json:"-"` - - Request `json:"request"` - Error string `json:"error"` - NotifyUser *query.NotifyUser `json:"notifyUser"` - BackOff time.Duration `json:"backOff"` -} - -func (e *RetryRequestedEvent) Payload() interface{} { - return e -} - -func (e *RetryRequestedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil -} - -func (e *RetryRequestedEvent) SetBaseEvent(event *eventstore.BaseEvent) { - e.BaseEvent = *event -} - -func NewRetryRequestedEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - userID, - userResourceOwner, - aggregateID, - aggregateResourceOwner, - triggerOrigin, - urlTemplate string, - code *crypto.CryptoValue, - codeExpiry time.Duration, - eventType eventstore.EventType, - notificationType domain.NotificationType, - messageType string, - unverifiedNotificationChannel, - isOTP bool, - args *domain.NotificationArguments, - notifyUser *query.NotifyUser, - backoff time.Duration, - errorMessage string, -) *RetryRequestedEvent { - return &RetryRequestedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( - ctx, - aggregate, - RetryRequestedType, - ), - Request: Request{ - UserID: userID, - UserResourceOwner: userResourceOwner, - AggregateID: aggregateID, - AggregateResourceOwner: aggregateResourceOwner, - TriggeredAtOrigin: triggerOrigin, - EventType: eventType, - MessageType: messageType, - NotificationType: notificationType, - URLTemplate: urlTemplate, - CodeExpiry: codeExpiry, - Code: code, - UnverifiedNotificationChannel: unverifiedNotificationChannel, - IsOTP: isOTP, - Args: args, - }, - NotifyUser: notifyUser, - BackOff: backoff, - Error: errorMessage, - } +func (e *Request) Kind() string { + return "notification_request" } diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index a2bc5c7abd..0070f71a95 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -56,6 +56,7 @@ func NewOAuthIDPAddedEvent( userEndpoint, idAttribute string, scopes []string, + usePKCE bool, options idp.Options, ) *OAuthIDPAddedEvent { @@ -75,6 +76,7 @@ func NewOAuthIDPAddedEvent( userEndpoint, idAttribute, scopes, + usePKCE, options, ), } @@ -137,7 +139,7 @@ func NewOIDCIDPAddedEvent( clientID string, clientSecret *crypto.CryptoValue, scopes []string, - isIDTokenMapping bool, + isIDTokenMapping, usePKCE bool, options idp.Options, ) *OIDCIDPAddedEvent { @@ -155,6 +157,7 @@ func NewOIDCIDPAddedEvent( clientSecret, scopes, isIDTokenMapping, + usePKCE, options, ), } @@ -852,6 +855,7 @@ func NewLDAPIDPAddedEvent( userObjectClasses []string, userFilters []string, timeout time.Duration, + rootCA []byte, attributes idp.LDAPAttributes, options idp.Options, ) *LDAPIDPAddedEvent { @@ -874,6 +878,7 @@ func NewLDAPIDPAddedEvent( userObjectClasses, userFilters, timeout, + rootCA, attributes, options, ), diff --git a/internal/repository/permission/eventstore.go b/internal/repository/permission/eventstore.go new file mode 100644 index 0000000000..65a56d1b74 --- /dev/null +++ b/internal/repository/permission/eventstore.go @@ -0,0 +1,8 @@ +package permission + +import "github.com/zitadel/zitadel/internal/eventstore" + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, AddedType, eventstore.GenericEventMapper[AddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, RemovedType, eventstore.GenericEventMapper[RemovedEvent]) +} diff --git a/internal/repository/permission/permission.go b/internal/repository/permission/permission.go index a02a4dca0a..7e49093c51 100644 --- a/internal/repository/permission/permission.go +++ b/internal/repository/permission/permission.go @@ -99,7 +99,7 @@ func (e *RemovedEvent) Fields() []*eventstore.FieldOperation { func NewRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, role, permission string) *RemovedEvent { return &RemovedEvent{ - BaseEvent: eventstore.NewBaseEventForPush(ctx, aggregate, AddedType), + BaseEvent: eventstore.NewBaseEventForPush(ctx, aggregate, RemovedType), Role: role, Permission: permission, } diff --git a/internal/repository/project/oidc_config.go b/internal/repository/project/oidc_config.go index 8bc918afbe..dd7d3a85b6 100644 --- a/internal/repository/project/oidc_config.go +++ b/internal/repository/project/oidc_config.go @@ -384,13 +384,13 @@ func ChangeBackChannelLogoutURI(backChannelLogoutURI string) func(event *OIDCCon } } -func ChangeLoginVersion(loginVersion domain.LoginVersion) func(event *OIDCConfigChangedEvent) { +func ChangeOIDCLoginVersion(loginVersion domain.LoginVersion) func(event *OIDCConfigChangedEvent) { return func(e *OIDCConfigChangedEvent) { e.LoginVersion = &loginVersion } } -func ChangeLoginBaseURI(loginBaseURI string) func(event *OIDCConfigChangedEvent) { +func ChangeOIDCLoginBaseURI(loginBaseURI string) func(event *OIDCConfigChangedEvent) { return func(e *OIDCConfigChangedEvent) { e.LoginBaseURI = &loginBaseURI } diff --git a/internal/repository/project/saml_config.go b/internal/repository/project/saml_config.go index 97af24a0d9..ddcb9c0eab 100644 --- a/internal/repository/project/saml_config.go +++ b/internal/repository/project/saml_config.go @@ -3,6 +3,7 @@ package project import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -16,10 +17,12 @@ const ( type SAMLConfigAddedEvent struct { eventstore.BaseEvent `json:"-"` - AppID string `json:"appId"` - EntityID string `json:"entityId"` - Metadata []byte `json:"metadata,omitempty"` - MetadataURL string `json:"metadata_url,omitempty"` + AppID string `json:"appId"` + EntityID string `json:"entityId"` + Metadata []byte `json:"metadata,omitempty"` + MetadataURL string `json:"metadata_url,omitempty"` + LoginVersion domain.LoginVersion `json:"loginVersion,omitempty"` + LoginBaseURI string `json:"loginBaseURI,omitempty"` } func (e *SAMLConfigAddedEvent) Payload() interface{} { @@ -50,6 +53,8 @@ func NewSAMLConfigAddedEvent( entityID string, metadata []byte, metadataURL string, + loginVersion domain.LoginVersion, + loginBaseURI string, ) *SAMLConfigAddedEvent { return &SAMLConfigAddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -57,10 +62,12 @@ func NewSAMLConfigAddedEvent( aggregate, SAMLConfigAddedType, ), - AppID: appID, - EntityID: entityID, - Metadata: metadata, - MetadataURL: metadataURL, + AppID: appID, + EntityID: entityID, + Metadata: metadata, + MetadataURL: metadataURL, + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, } } @@ -80,11 +87,13 @@ func SAMLConfigAddedEventMapper(event eventstore.Event) (eventstore.Event, error type SAMLConfigChangedEvent struct { eventstore.BaseEvent `json:"-"` - AppID string `json:"appId"` - EntityID string `json:"entityId"` - Metadata []byte `json:"metadata,omitempty"` - MetadataURL *string `json:"metadata_url,omitempty"` - oldEntityID string + AppID string `json:"appId"` + EntityID string `json:"entityId"` + Metadata []byte `json:"metadata,omitempty"` + MetadataURL *string `json:"metadata_url,omitempty"` + LoginVersion *domain.LoginVersion `json:"loginVersion,omitempty"` + LoginBaseURI *string `json:"loginBaseURI,omitempty"` + oldEntityID string } func (e *SAMLConfigChangedEvent) Payload() interface{} { @@ -147,6 +156,17 @@ func ChangeEntityID(entityID string) func(event *SAMLConfigChangedEvent) { } } +func ChangeSAMLLoginVersion(loginVersion domain.LoginVersion) func(event *SAMLConfigChangedEvent) { + return func(e *SAMLConfigChangedEvent) { + e.LoginVersion = &loginVersion + } +} +func ChangeSAMLLoginBaseURI(loginBaseURI string) func(event *SAMLConfigChangedEvent) { + return func(e *SAMLConfigChangedEvent) { + e.LoginBaseURI = &loginBaseURI + } +} + func SAMLConfigChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { e := &SAMLConfigChangedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/internal/repository/samlrequest/saml_request.go b/internal/repository/samlrequest/saml_request.go index b3ecdd753e..aca8da99fe 100644 --- a/internal/repository/samlrequest/saml_request.go +++ b/internal/repository/samlrequest/saml_request.go @@ -19,14 +19,15 @@ const ( type AddedEvent struct { *eventstore.BaseEvent `json:"-"` - LoginClient string `json:"login_client,omitempty"` - ApplicationID string `json:"application_id,omitempty"` - ACSURL string `json:"acs_url,omitempty"` - RelayState string `json:"relay_state,omitempty"` - RequestID string `json:"request_id,omitempty"` - Binding string `json:"binding,omitempty"` - Issuer string `json:"issuer,omitempty"` - Destination string `json:"destination,omitempty"` + LoginClient string `json:"login_client,omitempty"` + ApplicationID string `json:"application_id,omitempty"` + ACSURL string `json:"acs_url,omitempty"` + RelayState string `json:"relay_state,omitempty"` + RequestID string `json:"request_id,omitempty"` + Binding string `json:"binding,omitempty"` + Issuer string `json:"issuer,omitempty"` + Destination string `json:"destination,omitempty"` + ResponseIssuer string `json:"response_issuer,omitempty"` } func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { @@ -51,6 +52,7 @@ func NewAddedEvent(ctx context.Context, binding string, issuer string, destination string, + responseIssuer string, ) *AddedEvent { return &AddedEvent{ BaseEvent: eventstore.NewBaseEventForPush( @@ -58,14 +60,15 @@ func NewAddedEvent(ctx context.Context, aggregate, AddedType, ), - LoginClient: loginClient, - ApplicationID: applicationID, - ACSURL: acsURL, - RelayState: relayState, - RequestID: requestID, - Binding: binding, - Issuer: issuer, - Destination: destination, + LoginClient: loginClient, + ApplicationID: applicationID, + ACSURL: acsURL, + RelayState: relayState, + RequestID: requestID, + Binding: binding, + Issuer: issuer, + Destination: destination, + ResponseIssuer: responseIssuer, } } diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index 42304aca8e..7aad348841 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -660,7 +660,7 @@ func NewLifetimeSetEvent( type TerminateEvent struct { eventstore.BaseEvent `json:"-"` - TriggerOrigin string `json:"triggerOrigin,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` } func (e *TerminateEvent) Payload() interface{} { @@ -671,6 +671,10 @@ func (e *TerminateEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return nil } +func (e *TerminateEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + func NewTerminateEvent( ctx context.Context, aggregate *eventstore.Aggregate, @@ -681,6 +685,7 @@ func NewTerminateEvent( aggregate, TerminateType, ), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/static/database/crdb.go b/internal/static/database/crdb.go index a031f7d17a..549e0ae505 100644 --- a/internal/static/database/crdb.go +++ b/internal/static/database/crdb.go @@ -14,7 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -var _ static.Storage = (*crdbStorage)(nil) +var _ static.Storage = (*storage)(nil) const ( assetsTable = "system.assets" @@ -29,15 +29,15 @@ const ( AssetColUpdatedAt = "updated_at" ) -type crdbStorage struct { +type storage struct { client *sql.DB } func NewStorage(client *sql.DB, _ map[string]interface{}) (static.Storage, error) { - return &crdbStorage{client: client}, nil + return &storage{client: client}, nil } -func (c *crdbStorage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) { +func (c *storage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) { data, err := io.ReadAll(object) if err != nil { return nil, zerrors.ThrowInternal(err, "DATAB-Dfwvq", "Errors.Internal") @@ -71,7 +71,7 @@ func (c *crdbStorage) PutObject(ctx context.Context, instanceID, location, resou }, nil } -func (c *crdbStorage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) { +func (c *storage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) { query, args, err := squirrel.Select(AssetColData, AssetColContentType, AssetColHash, AssetColUpdatedAt). From(assetsTable). Where(squirrel.Eq{ @@ -111,7 +111,7 @@ func (c *crdbStorage) GetObject(ctx context.Context, instanceID, resourceOwner, nil } -func (c *crdbStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) { +func (c *storage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) { query, args, err := squirrel.Select(AssetColContentType, AssetColLocation, "length("+AssetColData+")", AssetColHash, AssetColUpdatedAt). From(assetsTable). Where(squirrel.Eq{ @@ -143,7 +143,7 @@ func (c *crdbStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwn return asset, nil } -func (c *crdbStorage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error { +func (c *storage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error { stmt, args, err := squirrel.Delete(assetsTable). Where(squirrel.Eq{ AssetColInstanceID: instanceID, @@ -162,7 +162,7 @@ func (c *crdbStorage) RemoveObject(ctx context.Context, instanceID, resourceOwne return nil } -func (c *crdbStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error { +func (c *storage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error { stmt, args, err := squirrel.Delete(assetsTable). Where(squirrel.Eq{ AssetColInstanceID: instanceID, @@ -181,7 +181,7 @@ func (c *crdbStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwn return nil } -func (c *crdbStorage) RemoveInstanceObjects(ctx context.Context, instanceID string) error { +func (c *storage) RemoveInstanceObjects(ctx context.Context, instanceID string) error { stmt, args, err := squirrel.Delete(assetsTable). Where(squirrel.Eq{ AssetColInstanceID: instanceID, diff --git a/internal/static/database/crdb_test.go b/internal/static/database/crdb_test.go index 14a128dbe2..2be76e69fa 100644 --- a/internal/static/database/crdb_test.go +++ b/internal/static/database/crdb_test.go @@ -40,7 +40,7 @@ const ( " WHERE instance_id = $1" ) -func Test_crdbStorage_CreateObject(t *testing.T) { +func Test_dbStorage_CreateObject(t *testing.T) { type fields struct { client db } @@ -112,7 +112,7 @@ func Test_crdbStorage_CreateObject(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &crdbStorage{ + c := &storage{ client: tt.fields.client.db, } got, err := c.PutObject(tt.args.ctx, tt.args.instanceID, tt.args.location, tt.args.resourceOwner, tt.args.name, tt.args.contentType, tt.args.objectType, tt.args.data, tt.args.objectSize) @@ -127,7 +127,7 @@ func Test_crdbStorage_CreateObject(t *testing.T) { } } -func Test_crdbStorage_RemoveObject(t *testing.T) { +func Test_dbStorage_RemoveObject(t *testing.T) { type fields struct { client db } @@ -166,7 +166,7 @@ func Test_crdbStorage_RemoveObject(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &crdbStorage{ + c := &storage{ client: tt.fields.client.db, } err := c.RemoveObject(tt.args.ctx, tt.args.instanceID, tt.args.resourceOwner, tt.args.name) @@ -178,7 +178,7 @@ func Test_crdbStorage_RemoveObject(t *testing.T) { } } -func Test_crdbStorage_RemoveObjects(t *testing.T) { +func Test_dbStorage_RemoveObjects(t *testing.T) { type fields struct { client db } @@ -216,7 +216,7 @@ func Test_crdbStorage_RemoveObjects(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &crdbStorage{ + c := &storage{ client: tt.fields.client.db, } err := c.RemoveObjects(tt.args.ctx, tt.args.instanceID, tt.args.resourceOwner, tt.args.objectType) @@ -227,7 +227,7 @@ func Test_crdbStorage_RemoveObjects(t *testing.T) { }) } } -func Test_crdbStorage_RemoveInstanceObjects(t *testing.T) { +func Test_dbStorage_RemoveInstanceObjects(t *testing.T) { type fields struct { client db } @@ -260,7 +260,7 @@ func Test_crdbStorage_RemoveInstanceObjects(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &crdbStorage{ + c := &storage{ client: tt.fields.client.db, } err := c.RemoveInstanceObjects(tt.args.ctx, tt.args.instanceID) diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 5539fabb12..d7dc18898b 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -561,6 +561,7 @@ Errors: AlreadyExists: Auth Request вече съществува NotExisting: Auth Request не съществува WrongLoginClient: Auth Request, създаден от друг клиент за влизане + AlreadyHandled: Заявката за удостоверяване вече е обработена OIDCSession: RefreshTokenInvalid: Токенът за опресняване е невалиден Token: @@ -571,8 +572,12 @@ Errors: AlreadyExists: SAMLRequest вече съществува NotExisting: SAMLRequest не съществува WrongLoginClient: SAMLRequest, създаден от друг клиент за влизане + AlreadyHandled: SAML заявката вече е обработена SAMLSession: InvalidClient: SAMLResponse не е издаден за този клиент + DeviceAuth: + NotFound: Заявката за авторизация на устройство не съществува + AlreadyHandled: Заявката за авторизация на устройство вече е обработена Feature: NotExisting: Функцията не съществува TypeNotSupported: Типът функция не се поддържа diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 6a21286a2c..80db4952f9 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -541,6 +541,7 @@ Errors: AlreadyExists: Požadavek na autentizaci již existuje NotExisting: Požadavek na autentizaci neexistuje WrongLoginClient: Požadavek na autentizaci vytvořen jiným klientem přihlášení + AlreadyHandled: Žádost o ověření již byla zpracována OIDCSession: RefreshTokenInvalid: Obnovovací token je neplatný Token: @@ -551,8 +552,12 @@ Errors: AlreadyExists: SAMLRequest již existuje NotExisting: SAMLRequest neexistuje WrongLoginClient: SAMLRequest vytvořený jiným přihlašovacím klientem + AlreadyHandled: SAML požadavek již byl zpracován SAMLSession: InvalidClient: Pro tohoto klienta nebyla vydána odpověď SAMLResponse + DeviceAuth: + NotFound: Žádost o autorizaci zařízení neexistuje + AlreadyHandled: Žádost o autorizaci zařízení již byla zpracována Feature: NotExisting: Funkce neexistuje TypeNotSupported: Typ funkce není podporován diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 117bbcb897..dcb3ac5c71 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Auth Request existiert bereits NotExisting: Auth Request existiert nicht WrongLoginClient: Auth Request wurde von einem anderen Login-Client erstellt + AlreadyHandled: Auth Request wurde bereits bearbeitet OIDCSession: RefreshTokenInvalid: Refresh Token ist ungültig Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest existiert bereits NotExisting: SAMLRequest existiert nicht WrongLoginClient: SAMLRequest wurde con einem andere Login-Client erstellt + AlreadyHandled: SAMLRequest wurde bereits bearbeitet SAMLSession: InvalidClient: SAMLResponse wurde nicht für diesen Client ausgestellt + DeviceAuth: + NotFound: Die Geräteautorisierungsanforderung existiert nicht + AlreadyHandled: Die Geräteautorisierungsanforderung wurde bereits bearbeitet Feature: NotExisting: Feature existiert nicht TypeNotSupported: Feature Typ wird nicht unterstützt diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 618f4a500a..bd8d26d727 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -544,6 +544,7 @@ Errors: AlreadyExists: Auth Request already exists NotExisting: Auth Request does not exist WrongLoginClient: Auth Request created by other login client + AlreadyHandled: Auth Request has already been handled OIDCSession: RefreshTokenInvalid: Refresh Token is invalid Token: @@ -554,8 +555,12 @@ Errors: AlreadyExists: SAMLRequest already exists NotExisting: SAMLRequest does not exist WrongLoginClient: SAMLRequest created by other login client + AlreadyHandled: SAMLRequest has already been handled SAMLSession: InvalidClient: SAMLResponse was not issued for this client + DeviceAuth: + NotFound: Device Authorization Request does not exist + AlreadyHandled: Device Authorization Request has already been handled Feature: NotExisting: Feature does not exist TypeNotSupported: Feature type is not supported diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index b2c0c0a685..9f11b63964 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Auth Request ya existe NotExisting: Auth Request no existe WrongLoginClient: Auth Request creado por otro cliente de inicio de sesión + AlreadyHandled: Auth Request ya ha sido procesada OIDCSession: RefreshTokenInvalid: El token de refresco no es válido Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest ya existe NotExisting: SAMLRequest no existe WrongLoginClient: SAMLRequest creado por otro cliente de inicio de sesión + AlreadyHandled: SAMLRequest ya ha sido procesada SAMLSession: InvalidClient: SAMLResponse no ha sido emitido para este cliente + DeviceAuth: + NotFound: La solicitud de autorización del dispositivo no existe + AlreadyHandled: La solicitud de autorización del dispositivo ya ha sido procesada Feature: NotExisting: La característica no existe TypeNotSupported: El tipo de característica no es compatible diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index eb143e592c..ff8393befc 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Auth Request existe déjà NotExisting: Auth Request n'existe pas WrongLoginClient: Auth Request créé par un autre client de connexion + AlreadyHandled: Auth Request a déjà été traitée OIDCSession: RefreshTokenInvalid: Le jeton de rafraîchissement n'est pas valide Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest existe déjà NotExisting: SAMLRequest n'existe pas WrongLoginClient: SAMLRequest créé par un autre client de connexion + AlreadyHandled: SAMLRequest a déjà été traitée SAMLSession: InvalidClient: SAMLResponse n'a pas été émise pour ce client + DeviceAuth: + NotFound: La demande d'autorisation de l'appareil n'existe pas + AlreadyHandled: La demande d'autorisation de l'appareil a déjà été traitée Feature: NotExisting: La fonctionnalité n'existe pas TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml index d33b5f47bc..b17c6a1225 100644 --- a/internal/static/i18n/hu.yaml +++ b/internal/static/i18n/hu.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Az Auth Request már létezik NotExisting: Az Auth Request nem létezik WrongLoginClient: Az Auth Requestet egy másik bejelentkezési kliens hozta létre + AlreadyHandled: A hitelesítési kérelem már feldolgozva OIDCSession: RefreshTokenInvalid: Az Refresh Token érvénytelen Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: A SAMLRequest már létezik NotExisting: A SAMLRequest nem létezik WrongLoginClient: A SAMLRequest egy másik bejelentkezési ügyfél által létrehozott + AlreadyHandled: A SAMLRequest már feldolgozva SAMLSession: InvalidClient: SAMLResponse nem lett kiadva ehhez az ügyfélhez + DeviceAuth: + NotFound: Az eszközengedélyezési kérelem nem létezik + AlreadyHandled: Az eszközengedélyezési kérelem már feldolgozva Feature: NotExisting: A funkció nem létezik TypeNotSupported: A funkció típusa nem támogatott diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index 449f91ffdc..56a454e71d 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Permintaan Otentikasi sudah ada NotExisting: Permintaan Otentikasi tidak ada WrongLoginClient: Permintaan Otentikasi dibuat oleh klien login lain + AlreadyHandled: Permintaan Otentikasi sudah ditangani OIDCSession: RefreshTokenInvalid: Token Penyegaran tidak valid Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest sudah ada NotExisting: SAMLRequest tidak ada WrongLoginClient: SAMLRequest dibuat oleh klien login lainnya + AlreadyHandled: SAMLRequest sudah ditangani SAMLSession: InvalidClient: SAMLResponse tidak dikeluarkan untuk klien ini + DeviceAuth: + NotFound: Permintaan Otorisasi Perangkat tidak ada + AlreadyHandled: Permintaan Otorisasi Perangkat sudah ditangani Feature: NotExisting: Fitur tidak ada TypeNotSupported: Jenis fitur tidak didukung diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index a94925a906..6713abf2e1 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Auth Request esiste già NotExisting: Auth Request non esiste WrongLoginClient: Auth Request creato da un altro client di accesso + AlreadyHandled: Auth Request è già stata gestita OIDCSession: RefreshTokenInvalid: Refresh Token non è valido Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest esiste già NotExisting: SAMLRequest non esiste WrongLoginClient: SAMLRequest creato da un altro client di accesso + AlreadyHandled: SAMLRequest è già stata gestita SAMLSession: InvalidClient: SAMLResponse non è stato emesso per questo client + DeviceAuth: + NotFound: La richiesta di autorizzazione del dispositivo non esiste + AlreadyHandled: La richiesta di autorizzazione del dispositivo è già stata gestita Feature: NotExisting: La funzionalità non esiste TypeNotSupported: Il tipo di funzionalità non è supportato diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 582e5037bb..f57d0f6661 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -53,6 +53,7 @@ Errors: NotFound: SMS構成が見つかりません AlreadyActive: このSMS構成はすでにアクティブです AlreadyDeactivated: このSMS構成はすでに非アクティブです + NotExternalVerification: SMS構成は外部のコード検証をサポートしていません SMTP: NotEmailMessage: メッセージは EmailMessage ではありません RequiredAttributes: 件名、受信者、コンテンツを設定する必要がありますが、一部またはすべてが空です @@ -98,16 +99,23 @@ Errors: Profile: NotFound: プロファイルが見つかりません NotChanged: プロファイルが変更されていません + Empty: プロファイルが空です + FirstNameEmpty: 名前が空です + LastNameEmpty: 姓が空です Invalid: プロファイルデータが無効です Email: NotFound: メールアドレスが見つかりません Invalid: 無効なメールアドレスです AlreadyVerified: メールアドレスはすでに検証済みです NotChanged: メールアドレスが変更されていません + Empty: メールアドレスが空です + IDMissing: メールアドレスIDが不足しています Phone: NotFound: 電話番号が見つかりません Invalid: 無効な電話番号です AlreadyVerified: 電話番号はすでに認証済みです + Empty: 電話番号が空です + NotChanged: 電話番号が変更されていません Address: NotFound: 住所が見つかりません NotChanged: 住所は変更されていません @@ -129,6 +137,7 @@ Errors: Username: AlreadyExists: ユーザー名はすでに使用されています Reserved: ユーザー名はすでに使用されています + Empty: ユーザー名が空です Code: Empty: コードは空です NotFound: コードが見つかりません @@ -276,6 +285,9 @@ Errors: NotFound: 通知ポリシーが見つかりません NotChanged: 通知ポリシーは変更されていません AlreadyExists: 通知ポリシーはすでに存在しています + LabelPolicy: + NotFound: プライベートラベルポリシーが見つかりません + NotChanged: プライベートラベルポリシーが変更されていません Project: ProjectIDMissing: プロジェクトIDがありません AlreadyExists: プロジェクトはすでに組織に存在しています @@ -532,6 +544,7 @@ Errors: AlreadyExists: AuthRequestはすでに存在する NotExisting: AuthRequest が存在しません WrongLoginClient: 他のログインクライアントによって作成された AuthRequest + AlreadyHandled: 認証リクエストは既に処理済みです OIDCSession: RefreshTokenInvalid: 無効なリフレッシュトークンです Token: @@ -542,8 +555,12 @@ Errors: AlreadyExists: SAMLリクエストはすでに存在します NotExisting: SAMLリクエストが存在しません WrongLoginClient: 他のログイン クライアントによって作成された SAMLRequest + AlreadyHandled: SAMLリクエストは既に処理済みです SAMLSession: InvalidClient: このクライアントに対してSAMLResponseは発行されませんでした + DeviceAuth: + NotFound: デバイス認証リクエストが存在しません + AlreadyHandled: デバイス認証リクエストは既に処理済みです Feature: NotExisting: 機能が存在しません TypeNotSupported: 機能タイプはサポートされていません @@ -710,6 +727,10 @@ EventTypes: check: succeeded: パスワードチェックの成功 failed: パスワードチェックの失敗 + change: + sent: パスワード変更メールを送信しました + hash: + updated: パスワードハッシュが更新されました externallogin: check: succeeded: 外部ログインの成功 @@ -813,10 +834,6 @@ EventTypes: check: succeeded: パスワードチェックの成功 failed: パスワードチェックの失敗 - change: - sent: パスワード変更を送信しました - hash: - updated: パスワードハッシュが更新されました phone: changed: 電話番号の変更 verified: 電話番号の検証 @@ -825,7 +842,7 @@ EventTypes: code: added: 電話番号コードの生成 sent: 電話番号コードの送信 - removed: 電話番号の削除 + profile: changed: ユーザープロファイルの変更 address: @@ -1048,6 +1065,9 @@ EventTypes: check: succeeded: OIDCクライアントシークレットチェックの成功 failed: OIDCクライアントシークレットチェックの失敗 + key: + added: OIDCアプリキーの追加 + removed: OIDCアプリキーの削除 api: secret: check: @@ -1345,6 +1365,7 @@ EventTypes: code: added: 電話番号の確認コードが生成されました sent: 電話番号の確認コードが送信されました + web_key: added: Web キーが追加されました activated: Web キーが有効化されました diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml index 741f075ca2..d238142e01 100644 --- a/internal/static/i18n/ko.yaml +++ b/internal/static/i18n/ko.yaml @@ -544,6 +544,7 @@ Errors: AlreadyExists: 인증 요청이 이미 존재합니다 NotExisting: 인증 요청이 존재하지 않습니다 WrongLoginClient: 다른 로그인 클라이언트에 의해 생성된 인증 요청 + AlreadyHandled: 인증 요청이 이미 처리되었습니다 OIDCSession: RefreshTokenInvalid: 새로 고침 토큰이 유효하지 않습니다 Token: @@ -554,8 +555,12 @@ Errors: AlreadyExists: SAMLRequest가 이미 존재합니다 NotExisting: SAMLRequest가 존재하지 않습니다 WrongLoginClient: 다른 로그인 클라이언트가 생성한 SAMLRequest + AlreadyHandled: SAML 요청이 이미 처리되었습니다 SAMLSession: InvalidClient: 이 클라이언트에 대해 SAMLResponse가 발행되지 않았습니다. + DeviceAuth: + NotFound: 장치 인증 요청이 존재하지 않습니다 + AlreadyHandled: 장치 인증 요청이 이미 처리되었습니다 Feature: NotExisting: 기능이 존재하지 않습니다 TypeNotSupported: 기능 유형이 지원되지 않습니다 diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index be205f5380..898ed67360 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -542,6 +542,7 @@ Errors: AlreadyExists: Барањето за автентикација веќе постои NotExisting: Барањето за автентикација не постои WrongLoginClient: Барањето за автификација беше креирано од друг клиент за најавување + AlreadyHandled: Барањето за автентикација е веќе обработено OIDCSession: RefreshTokenInvalid: Токенот за освежување е неважечки Token: @@ -552,8 +553,12 @@ Errors: AlreadyExists: SAMLRequest веќе постои NotExisting: SAMLRequest не постои WrongLoginClient: SAML Барање создадено од друг клиент за најавување + AlreadyHandled: SAML барањето е веќе обработено SAMLSession: InvalidClient: SAMLResponse не беше издаден за овој клиент + DeviceAuth: + NotFound: Барањето за авторизација на уредот не постои + AlreadyHandled: Барањето за авторизација на уредот е веќе обработено Feature: NotExisting: Функцијата не постои TypeNotSupported: Типот на функција не е поддржан diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 262b24f2fb..882c58a4f2 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Auth Verzoek bestaat al NotExisting: Auth Verzoek bestaat niet WrongLoginClient: Auth Verzoek aangemaakt door andere login client + AlreadyHandled: Authenticatieverzoek is al verwerkt OIDCSession: RefreshTokenInvalid: Refresh Token is ongeldig Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest bestaat al NotExisting: SAMLRequest bestaat niet WrongLoginClient: SAMLRequest aangemaakt door andere login client + AlreadyHandled: SAML-verzoek is al verwerkt SAMLSession: InvalidClient: SAMLResponse is niet uitgegeven voor deze client + DeviceAuth: + NotFound: Apparaatautorisatieverzoek bestaat niet + AlreadyHandled: Apparaatautorisatieverzoek is al verwerkt Feature: NotExisting: Functie bestaat niet TypeNotSupported: Functie type wordt niet ondersteund diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index cc511ec0c0..13125bc2a9 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Auth Request już istnieje NotExisting: Auth Request nie istnieje WrongLoginClient: Auth Request utworzony przez innego klienta logowania + AlreadyHandled: Żądanie uwierzytelnienia zostało już obsłużone OIDCSession: RefreshTokenInvalid: Refresh Token jest nieprawidłowy Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest już istnieje NotExisting: SAMLRequest nie istnieje WrongLoginClient: SAMLRequest utworzony przez innego klienta logowania + AlreadyHandled: Żądanie SAML zostało już obsłużone SAMLSession: InvalidClient: SAMLResponse nie został wydany dla tego klienta + DeviceAuth: + NotFound: Żądanie autoryzacji urządzenia nie istnieje + AlreadyHandled: Żądanie autoryzacji urządzenia zostało już obsłużone Feature: NotExisting: Funkcja nie istnieje TypeNotSupported: Typ funkcji nie jest obsługiwany diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index bd106ab259..4ab3573c2b 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -542,6 +542,7 @@ Errors: AlreadyExists: A solicitação de autenticação já existe NotExisting: A solicitação de autenticação não existe WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login + AlreadyHandled: O pedido de autenticação já foi processado OIDCSession: RefreshTokenInvalid: O Refresh Token é inválido Token: @@ -552,8 +553,12 @@ Errors: AlreadyExists: O SAMLRequest já existe NotExisting: O SAMLRequest não existe WrongLoginClient: SAMLRequest criado por outro cliente de login + AlreadyHandled: O pedido SAML já foi processado SAMLSession: InvalidClient: O SAMLResponse não foi emitido para este cliente + DeviceAuth: + NotFound: O pedido de autorização do dispositivo não existe + AlreadyHandled: O pedido de autorização do dispositivo já foi processado Feature: NotExisting: O recurso não existe TypeNotSupported: O tipo de recurso não é compatível diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml new file mode 100644 index 0000000000..48790da9e5 --- /dev/null +++ b/internal/static/i18n/ro.yaml @@ -0,0 +1,1413 @@ +Errors: + Internal: A apărut o eroare internă + NoChangesFound: Nicio modificare + OriginNotAllowed: Acest "Origin" nu este permis + IDMissing: ID lipsă + ResourceOwnerMissing: Organizația Proprietarului Resursei lipsă + RemoveFailed: Nu a putut fi eliminat + ProjectionName: + Invalid: Nume de proiecție invalid + Assets: + EmptyKey: Cheia activului este goală + Store: + NotInitialized: Stocarea activelor nu este inițializată + NotConfigured: Stocarea activelor nu este configurată + Bucket: + Internal: Eroare internă la crearea bucket-ului + AlreadyExists: Bucket-ul există deja + CreateFailed: Bucket-ul nu a fost creat + ListFailed: Bucket-urile nu au putut fi citite + RemoveFailed: Bucket-ul nu a fost șters + SetPublicFailed: Nu s-a putut seta bucket-ul ca public + Object: + PutFailed: Obiectul nu a fost creat + GetFailed: Obiectul nu a putut fi citit + NotFound: Obiectul nu a fost găsit + PresignedTokenFailed: Token-ul semnat nu a putut fi creat + ListFailed: Lista de obiecte nu a putut fi citită + RemoveFailed: Obiectul nu a putut fi eliminat + Limit: + ExceedsDefault: Limita depășește limita implicită + Limits: + NotFound: Limitele nu au fost găsite + NoneSpecified: Nu au fost specificate limite + Instance: + Blocked: Instanța este blocată + Restrictions: + NoneSpecified: Nu au fost specificate restricții + DefaultLanguageMustBeAllowed: Limba implicită trebuie să fie permisă + Language: + NotParsed: Nu s-a putut analiza limba + NotSupported: Limba nu este suportată + NotAllowed: Limba nu este permisă + Undefined: Limba este nedefinită + Duplicate: Există duplicate în liste de limbi + OIDCSettings: + NotFound: Configurația OIDC nu a fost găsită + AlreadyExists: Configurația OIDC există deja + SecretGenerator: + AlreadyExists: Generatorul de secrete există deja + TypeMissing: Tipul generatorului de secrete lipsește + NotFound: Generatorul de secrete nu a fost găsit + SMSConfig: + NotFound: Configurația SMS nu a fost găsită + AlreadyActive: Configurația SMS este deja activă + AlreadyDeactivated: Configurația SMS este deja dezactivată + NotExternalVerification: Configurația SMS nu suportă verificarea prin cod + SMTP: + NotEmailMessage: Mesajul nu este EmailMessage + RequiredAttributes: Subiectul, destinatarii și conținutul trebuie să fie setate, dar unele sau toate sunt goale + CouldNotSplit: Nu s-a putut împărți host și port pentru conectarea la smtp + CouldNotDial: Nu s-a putut contacta serverul SMTP, verificați portul, probleme de firewall... + CouldNotDialTLS: Nu s-a putut contacta serverul SMTP folosind TLS, verificați portul, probleme de firewall... + CouldNotCreateClient: Nu s-a putut crea clientul smtp + CouldNotStartTLS: Nu s-a putut porni tls + CouldNotAuth: Nu s-a putut adăuga autentificare smtp, verificați dacă numele de utilizator și parola sunt corecte, dacă sunt corecte, poate furnizorul dvs. necesită o metodă de autentificare nesuportată de ZITADEL + CouldNotSetSender: Nu s-a putut seta expeditorul + CouldNotSetRecipient: Nu s-a putut seta destinatarul + SMTPConfig: + TestPassword: Parola pentru test nu a fost găsită + NotFound: Configurația SMTP nu a fost găsită + AlreadyExists: Configurația SMTP există deja + AlreadyDeactivated: Configurația SMTP este deja dezactivată + SenderAdressNotCustomDomain: Adresa expeditorului trebuie configurată ca domeniu personalizat pe instanță. + TestEmailNotFound: Adresa de e-mail pentru test nu a fost găsită + Notification: + NoDomain: Niciun domeniu găsit pentru mesaj + User: + NotFound: Utilizatorul nu a putut fi găsit + AlreadyExists: Utilizatorul există deja + NotFoundOnOrg: Utilizatorul nu a putut fi găsit în organizația aleasă + NotAllowedOrg: Utilizatorul nu este membru al organizației cerute + UserIDMissing: ID-ul utilizatorului lipsește + UserIDWrong: "Utilizatorul din cerere nu este egal cu utilizatorul autentificat" + DomainPolicyNil: Politica organizației este goală + EmailAsUsernameNotAllowed: E-mailul nu este permis ca nume de utilizator + Invalid: Datele utilizatorului sunt invalide + DomainNotAllowedAsUsername: Domeniul este deja rezervat și nu poate fi folosit + AlreadyInactive: Utilizatorul este deja inactiv + NotInactive: Utilizatorul nu este inactiv + CantDeactivateInitial: Utilizatorul cu starea inițială poate fi doar șters, nu dezactivat + ShouldBeActiveOrInitial: Utilizatorul nu este activ sau inițial + AlreadyInitialised: Utilizatorul este deja inițializat + NotInitialised: Utilizatorul nu este încă inițializat + NotLocked: Utilizatorul nu este blocat + NoChanges: Nu au fost găsite modificări + InitCodeNotFound: Codul de inițializare nu a fost găsit + UsernameNotChanged: Numele de utilizator nu a fost schimbat + InvalidURLTemplate: Șablonul URL este invalid + Profile: + NotFound: Profilul nu a fost găsit + NotChanged: Profilul nu a fost schimbat + Empty: Profilul este gol + FirstNameEmpty: Prenumele în profil este gol + LastNameEmpty: Numele de familie în profil este gol + IDMissing: ID-ul profilului lipsește + Email: + NotFound: E-mailul nu a fost găsit + Invalid: E-mailul este invalid + AlreadyVerified: E-mailul este deja verificat + NotChanged: E-mailul nu a fost schimbat + Empty: E-mailul este gol + IDMissing: ID-ul e-mailului lipsește + Phone: + NotFound: Numărul de telefon nu a fost găsit + Invalid: Numărul de telefon este invalid + AlreadyVerified: Numărul de telefon este deja verificat + Empty: Numărul de telefon este gol + NotChanged: Numărul de telefon nu a fost schimbat + Address: + NotFound: Adresa nu a fost găsită + NotChanged: Adresa nu a fost schimbată + Machine: + Key: + NotFound: Cheia mașinii nu a fost găsită + AlreadyExisting: Cheia mașinii există deja + Invalid: Cheia publică nu este o cheie publică RSA validă în format PKIX cu codificare PEM + Secret: + NotExisting: Secretul nu există + Invalid: Secretul este invalid + CouldNotGenerate: Secretul nu a putut fi generat + PAT: + NotFound: Token-ul de acces personal nu a fost găsit + NotHuman: Utilizatorul trebuie să fie personal + NotMachine: Utilizatorul trebuie să fie tehnic + WrongType: Nu este permis pentru acest tip de utilizator + NotAllowedToLink: Utilizatorului nu i se permite să se conecteze cu un furnizor de autentificare extern + Username: + AlreadyExists: Numele de utilizator este deja folosit + Reserved: Numele de utilizator este deja luat + Empty: Numele de utilizator este gol + Code: + Empty: Codul este gol + NotFound: Codul nu a fost găsit + Expired: Codul a expirat + GeneratorAlgNotSupported: Algoritm de generator neacceptat + Invalid: Codul este invalid + Password: + NotFound: Parola nu a fost găsită + Empty: Parola este goală + Invalid: Parola este invalidă + NotSet: Utilizatorul nu a setat o parolă + NotChanged: Parola nouă nu poate fi aceeași cu parola curentă + NotSupported: Codificarea hash a parolei nu este acceptată. Consultați https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets + PasswordComplexityPolicy: + NotFound: Politica de parolă nu a fost găsită + MinLength: Parola este prea scurtă + MinLengthNotAllowed: Lungimea minimă dată nu este permisă + HasLower: Parola trebuie să conțină litere mici + HasUpper: Parola trebuie să conțină litere mari + HasNumber: Parola trebuie să conțină numere + HasSymbol: Parola trebuie să conțină simboluri + ExternalIDP: + Invalid: IDP extern invalid + IDPConfigNotExisting: Furnizorul IDP este invalid pentru această organizație + NotAllowed: IDP extern nepermis + MinimumExternalIDPNeeded: Trebuie adăugat cel puțin un IDP + AlreadyExists: IDP extern deja luat + NotFound: IDP extern nu a fost găsit + LoginFailed: Conectarea la IDP extern a eșuat + MFA: + OTP: + AlreadyReady: Multifactor OTP (Parolă Unică) este deja configurat + NotExisting: Multifactor OTP (Parolă Unică) nu există + NotReady: Multifactor OTP (Parolă Unică) nu este pregătit + InvalidCode: Cod invalid + U2F: + NotExisting: U2F nu există + Passwordless: + NotExisting: Fără parolă nu există + WebAuthN: + NotFound: Token-ul WebAuthN nu a putut fi găsit + BeginRegisterFailed: Înregistrarea WebAuthN a început, dar a eșuat + MarshalError: Eroare la serializarea datelor + ErrorOnParseCredential: Eroare la analiza datelor de acreditare + CreateCredentialFailed: Eroare la crearea acreditărilor + BeginLoginFailed: Autentificarea WebAuthN a început, dar a eșuat + ValidateLoginFailed: Eroare la validarea acreditărilor de autentificare + CloneWarning: Acreditările pot fi clonate + RefreshToken: + Invalid: Token-ul de reîmprospătare este invalid + NotFound: Token-ul de reîmprospătare nu a fost găsit + Instance: + NotFound: Instanța nu a fost găsită + AlreadyExists: Instanța există deja + NotChanged: Instanța nu a fost schimbată + Org: + AlreadyExists: Numele organizației este deja luat + Invalid: Organizația este invalidă + AlreadyDeactivated: Organizația este deja dezactivată + AlreadyActive: Organizația este deja activă + Empty: Organizația este goală + NotFound: Organizația nu a fost găsită + NotChanged: Organizația nu a fost schimbată + DefaultOrgNotDeletable: Organizația implicită nu trebuie să fie ștearsă + ZitadelOrgNotDeletable: Organizația cu proiectul ZITADEL nu trebuie să fie ștearsă + InvalidDomain: Domeniu invalid + DomainMissing: Domeniu lipsă + DomainNotOnOrg: Domeniul nu există în organizație + DomainNotVerified: Domeniul nu este verificat + DomainAlreadyVerified: Domeniul este deja verificat + DomainVerificationTypeInvalid: Tipul de verificare a domeniului este invalid + DomainVerificationMissing: Verificarea domeniului nu a fost încă începută + DomainVerificationFailed: Verificarea domeniului a eșuat + DomainVerificationTXTNotFound: Înregistrarea TXT _zitadel-challenge nu a fost găsită pentru domeniul dvs. Verificați dacă l-ați adăugat la serverul DNS sau așteptați până când noua înregistrare este propagată + DomainVerificationTXTNoMatch: Înregistrarea TXT _zitadel-challenge a fost găsită pentru domeniul dvs., dar nu conține textul corect al token-ului. Verificați dacă ați adăugat token-ul corect la serverul DNS sau așteptați până când noua înregistrare este propagată + DomainVerificationHTTPNotFound: Fișierul care conține provocarea nu a fost găsit în URL-ul așteptat. Verificați dacă ați încărcat fișierul în locul potrivit cu permisiuni de citire + DomainVerificationHTTPNoMatch: Fișierul care conține provocarea a fost găsit în URL-ul așteptat, dar nu conține textul corect al token-ului. Verificați conținutul acestuia + DomainVerificationTimeout: A existat o depășire a timpului de așteptare la interogarea serverului DNS + PrimaryDomainNotDeletable: Domeniul principal nu trebuie să fie șters + DomainNotFound: Domeniul nu a fost găsit + MemberIDMissing: ID-ul membrului lipsește + MemberNotFound: Membrul organizației nu a fost găsit + InvalidMember: Membrul organizației este invalid + UserIDMissing: ID-ul utilizatorului lipsește + PolicyAlreadyExists: Politica există deja + PolicyNotExisting: Politica nu există + IdpInvalid: Configurația IDP este invalidă + IdpNotExisting: Configurația IDP nu există + OIDCConfigInvalid: Configurația OIDC IDP este invalidă + IdpIsNotOIDC: Configurația IDP nu este de tip oidc + Domain: + AlreadyExists: Domeniul există deja + InvalidCharacter: Numai caractere alfanumerice, . și - sunt permise pentru un domeniu + EmptyString: Caracterele non-numerice și alfabetice invalide au fost înlocuite cu spații goale și domeniul rezultat este un șir gol + IDP: + InvalidSearchQuery: Interogare de căutare invalidă + ClientIDMissing: ClientID lipsă + TeamIDMissing: TeamID lipsă + KeyIDMissing: KeyID lipsă + PrivateKeyMissing: Cheie Privată lipsă + LoginPolicy: + NotFound: Politica de conectare nu a fost găsită + Invalid: Politica de conectare este invalidă + RedirectURIInvalid: URI-ul de redirecționare implicit este invalid + NotExisting: Politica de conectare nu există + AlreadyExists: Politica de conectare există deja + IdpProviderAlreadyExisting: Furnizorul de identitate există deja + IdpProviderNotExisting: Furnizorul de identitate nu există + RegistrationNotAllowed: Înregistrarea nu este permisă + UsernamePasswordNotAllowed: Conectarea cu nume de utilizator / parolă nu este permisă + MFA: + AlreadyExists: Multifactor există deja + NotExisting: Multifactor nu există + Unspecified: Multifactor invalid + MailTemplate: + NotFound: Șablonul de e-mail implicit nu a fost găsit + NotChanged: Șablonul de e-mail implicit nu a fost schimbat + AlreadyExists: Șablonul de e-mail implicit există deja + Invalid: Șablonul de e-mail implicit este invalid + CustomMessageText: + NotFound: Textul mesajului implicit nu a fost găsit + NotChanged: Textul mesajului implicit nu a fost schimbat + AlreadyExists: Textul mesajului implicit există deja + Invalid: Textul mesajului implicit este invalid + PasswordComplexityPolicy: + NotFound: Politica de complexitate a parolei nu a fost găsită + Empty: Politica de complexitate a parolei este goală + NotExisting: Politica de complexitate a parolei nu există + AlreadyExists: Politica de complexitate a parolei există deja + PasswordLockoutPolicy: + NotFound: Politica de blocare a parolei nu a fost găsită + Empty: Politica de blocare a parolei este goală + NotExisting: Politica de blocare a parolei nu există + AlreadyExists: Politica de blocare a parolei există deja + PasswordAgePolicy: + NotFound: Politica de vârstă a parolei nu a fost găsită + Empty: Politica de vârstă a parolei este goală + NotExisting: Politica de vârstă a parolei nu există + AlreadyExists: Politica de vârstă a parolei există deja + OrgIAMPolicy: + Empty: Politica IAM a organizației este goală + NotExisting: Politica IAM a organizației nu există + AlreadyExists: Politica IAM a organizației există deja + NotificationPolicy: + NotFound: Politica de notificare nu a fost găsită + NotChanged: Politica de notificare nu a fost schimbată + AlreadyExists: Politica de notificare există deja + LabelPolicy: + NotFound: Politica de etichete private nu a fost găsită + NotChanged: Politica de etichete private nu a fost schimbată + Project: + ProjectIDMissing: ID-ul proiectului lipsește + AlreadyExists: Proiectul există deja în organizație + OrgNotExisting: Organizația nu există + UserNotExisting: Utilizatorul nu există + CouldNotGenerateClientSecret: Nu s-a putut genera secretul clientului + Invalid: Proiectul este invalid + NotActive: Proiectul nu este activ + NotInactive: Proiectul nu este dezactivat + NotFound: Proiectul nu a fost găsit + UserIDMissing: ID-ul utilizatorului lipsește + Member: + NotFound: Membrul proiectului nu a fost găsit + Invalid: Membrul proiectului este invalid + AlreadyExists: Membrul proiectului există deja + NotExisting: Membrul proiectului nu există + MinimumOneRoleNeeded: Trebuie adăugat cel puțin un rol + Role: + AlreadyExists: Rolul există deja + Invalid: Rolul este invalid + NotExisting: Rolul nu există + IDMissing: ID lipsă + App: + AlreadyExists: Aplicația există deja + NotFound: Aplicația nu a fost găsită + Invalid: Aplicația este invalidă + NotExisting: Aplicația nu există + NotActive: Aplicația nu este activă + NotInactive: Aplicația nu este inactivă + OIDCConfigInvalid: Configurația OIDC este invalidă + APIConfigInvalid: Configurația API este invalidă + SAMLConfigInvalid: Configurația SAML este invalidă + IsNotOIDC: Aplicația nu este de tip OIDC + IsNotAPI: Aplicația nu este de tip API + IsNotSAML: Aplicația nu este de tip SAML + SAMLMetadataMissing: Lipsesc metadatele SAML + SAMLMetadataFormat: Eroare de formatare a metadatelor SAML + SAMLEntityIDAlreadyExisting: SAML EntityID există deja + OIDCAuthMethodNoSecret: Metoda de autentificare OIDC aleasă nu necesită un secret + APIAuthMethodNoSecret: Metoda de autentificare API aleasă nu necesită un secret + AuthMethodNoPrivateKeyJWT: Metoda de autentificare aleasă nu necesită o cheie + ClientSecretInvalid: Secretul clientului este invalid + Key: + AlreadyExisting: Cheia aplicației există deja + NotFound: Cheia aplicației nu a fost găsită + RequiredFieldsMissing: Unele câmpuri obligatorii lipsesc + Grant: + AlreadyExists: Acordarea proiectului există deja + NotFound: Acordarea nu a fost găsită + Invalid: Acordarea proiectului este invalidă + NotExisting: Acordarea proiectului nu există + HasNotExistingRole: Un rol nu există în proiect + NotActive: Acordarea proiectului nu este activă + NotInactive: Acordarea proiectului nu este inactivă + IAM: + NotFound: Instanța nu a fost găsită. Asigurați-vă că aveți domeniul corect. Consultați https://zitadel.com/docs/apis/introduction#domains + Member: + RolesNotChanged: Rolurile nu au fost schimbate + MemberInvalid: Membrul este invalid + MemberAlreadyExisting: Membrul există deja + MemberNotExisting: Membrul nu există + IDMissing: Id lipsă + IAMProjectIDMissing: ID-ul proiectului IAM lipsește + IamProjectAlreadySet: ID-ul proiectului IAM a fost deja setat + IdpInvalid: Configurația IDP este invalidă + IdpNotExisting: Configurația IDP nu există + OIDCConfigInvalid: Configurația OIDC IDP este invalidă + IdpIsNotOIDC: Configurația IDP nu este de tip oidc + LoginPolicyInvalid: Politica de conectare este invalidă + LoginPolicyNotExisting: Politica de conectare nu există + IdpProviderInvalid: Furnizorul de identitate este invalid + LoginPolicy: + NotFound: Politica de conectare implicită nu a fost găsită + NotChanged: Politica de conectare implicită nu a fost schimbată + NotExisting: Politica de conectare implicită nu există + AlreadyExists: Politica de conectare implicită există deja + RedirectURIInvalid: URI-ul de redirecționare implicit este invalid + MFA: + AlreadyExists: Multifactor există deja + NotExisting: Multifactor nu există + Unspecified: Multifactor invalid + IDP: + AlreadyExists: Furnizorul de identitate există deja + NotExisting: Furnizorul de identitate nu există + Invalid: Furnizorul de identitate este invalid + IDPConfig: + AlreadyExists: Configurația furnizorului de identitate există deja + NotInactive: Configurația furnizorului de identitate nu este inactivă + NotActive: Configurația furnizorului de identitate nu este activă + LabelPolicy: + NotFound: Politica de etichete private implicită nu a fost găsită + NotChanged: Politica de etichete private implicită nu a fost schimbată + MailTemplate: + NotFound: Șablonul de e-mail implicit nu a fost găsit + NotChanged: Șablonul de e-mail implicit nu a fost schimbat + AlreadyExists: Șablonul de e-mail implicit există deja + Invalid: Șablonul de e-mail implicit este invalid + CustomMessageText: + NotFound: Textul mesajului implicit nu a fost găsit + NotChanged: Textul mesajului implicit nu a fost schimbat + AlreadyExists: Textul mesajului implicit există deja + Invalid: Textul mesajului implicit este invalid + PasswordComplexityPolicy: + NotFound: Politica implicită de complexitate a parolei nu a fost găsită + NotExisting: Politica implicită de complexitate a parolei nu există + AlreadyExists: Politica implicită de complexitate a parolei există deja + Empty: Politica implicită de complexitate a parolei este goală + NotChanged: Politica implicită de complexitate a parolei nu a fost schimbată + PasswordAgePolicy: + NotFound: Politica implicită de vârstă a parolei nu a fost găsită + NotExisting: Politica implicită de vârstă a parolei nu există + AlreadyExists: Politica implicită de vârstă a parolei există deja + Empty: Politica implicită de vârstă a parolei este goală + NotChanged: Politica implicită de vârstă a parolei nu a fost schimbată + PasswordLockoutPolicy: + NotFound: Politica implicită de blocare a parolei nu a fost găsită + NotExisting: Politica implicită de blocare a parolei nu există + AlreadyExists: Politica implicită de blocare a parolei există deja + Empty: Politica implicită de blocare a parolei este goală + NotChanged: Politica implicită de blocare a parolei nu a fost schimbată + DomainPolicy: + NotFound: Politica IAM a organizației nu a fost găsită + Empty: Politica IAM a organizației este goală + NotExisting: Politica IAM a organizației nu există + AlreadyExists: Politica IAM a organizației există deja + NotChanged: Politica IAM a organizației nu a fost schimbată + NotificationPolicy: + NotFound: Politica de notificare implicită nu a fost găsită + NotChanged: Politica de notificare implicită nu a fost schimbată + AlreadyExists: Politica de notificare implicită există deja + Policy: + AlreadyExists: Politica există deja + Label: + Invalid: + PrimaryColor: Culoarea primară nu este o valoare de culoare Hex validă + BackgroundColor: Culoarea de fundal nu este o valoare de culoare Hex validă + WarnColor: Culoarea de avertizare nu este o valoare de culoare Hex validă + FontColor: Culoarea fontului nu este o valoare de culoare Hex validă + PrimaryColorDark: Culoarea primară (modul întunecat) nu este o valoare de culoare Hex validă + BackgroundColorDark: Culoarea de fundal (modul întunecat) nu este o valoare de culoare Hex validă + WarnColorDark: Culoarea de avertizare (modul întunecat) nu este o valoare de culoare Hex validă + FontColorDark: Culoarea fontului (modul întunecat) nu este o valoare de culoare Hex validă + UserGrant: + AlreadyExists: Acordarea utilizatorului există deja + NotFound: Acordarea utilizatorului nu a fost găsită + Invalid: Acordarea utilizatorului este invalidă + NotChanged: Acordarea utilizatorului nu a fost schimbată + IDMissing: Id lipsă + NotActive: Acordarea utilizatorului nu este activă + NotInactive: Acordarea utilizatorului nu este dezactivată + NoPermissionForProject: Utilizatorul nu are permisiuni pentru acest proiect + RoleKeyNotFound: Rolul nu a fost găsit + Member: + AlreadyExists: Membrul există deja + IDPConfig: + AlreadyExists: Configurația IDP cu acest nume există deja + NotExisting: Configurația furnizorului de identitate nu există + Changes: + NotFound: Niciun istoric găsit + AuditRetention: Istoricul este în afara perioadei de păstrare a jurnalului de audit + Token: + NotFound: Token-ul nu a fost găsit + Invalid: Token-ul este invalid + UserSession: + NotFound: Sesiunea utilizatorului nu a fost găsită + Key: + NotFound: Cheia nu a fost găsită + ExpireBeforeNow: Data de expirare este în trecut + Login: + LoginPolicy: + MFA: + ForceAndNotConfigured: Multifactor este configurat ca obligatoriu, dar nu sunt configurați furnizori posibili. Vă rugăm să contactați administratorul sistemului. + Step: + Started: + AlreadyExists: Pasul a început deja, există deja + Done: + AlreadyExists: Pasul a terminat deja, există deja + CustomText: + AlreadyExists: Textul personalizat există deja + Invalid: Textul personalizat este invalid + NotFound: Textul personalizat nu a fost găsit + TranslationFile: + ReadError: Eroare la citirea fișierului de traducere + MergeError: Fișierul de traducere nu a putut fi îmbinat cu traducerile personalizate + NotFound: Fișierul de traducere nu există + Metadata: + NotFound: Metadatele nu au fost găsite + NoData: Lista de metadate este goală + Invalid: Metadatele sunt invalide + KeyNotExisting: Una sau mai multe chei nu există + Action: + Invalid: Acțiunea este invalidă + NotFound: Acțiunea nu a fost găsită + NotActive: Acțiunea nu este activă + NotInactive: Acțiunea nu este inactivă + MaxAllowed: Nu sunt permise acțiuni active suplimentare + NotEnabled: Caracteristica "Acțiune" nu este activată + Flow: + FlowTypeMissing: FlowType lipsă + Empty: Fluxul este deja gol + WrongTriggerType: TriggerType este invalid + NoChanges: Nicio modificare + ActionIDsNotExist: ActionIDs nu există + Query: + CloseRows: Declarația SQL nu a putut fi terminată + SQLStatement: Declarația SQL nu a putut fi creată + InvalidRequest: Cererea este invalidă + TooManyNestingLevels: Prea multe niveluri de imbricare a interogărilor (Max 20) + LimitExceeded: Limita a fost depășită + Quota: + AlreadyExists: Cota există deja pentru această unitate + NotFound: Cota nu a fost găsită pentru această unitate + Invalid: + CallURL: URL-ul de apel al cotei este invalid + Percent: Procentul cotei este mai mic de 1 + Unimplemented: Cotele nu sunt implementate pentru această unitate + Amount: Suma cotei este mai mică de 1 + ResetInterval: Intervalul de resetare a cotei este mai scurt de un minut + Noop: O cotă nelimitată fără notificări nu are efect + Access: + Exhausted: Cota pentru cererile autentificate este epuizată + Execution: + Exhausted: Cota pentru secunde de execuție este epuizată + LogStore: + Access: + StorageFailed: Stocarea jurnalului de acces în baza de date a eșuat + ScanFailed: Interogarea utilizării pentru cererile autentificate a eșuat + Execution: + StorageFailed: Stocarea jurnalului de execuție a acțiunii în baza de date a eșuat + ScanFailed: Interogarea utilizării pentru secunde de execuție a acțiunii a eșuat + Session: + NotExisting: Sesiunea nu există + Terminated: Sesiunea a fost deja terminată + Expired: Sesiunea a expirat + PositiveLifetime: Durata de viață a sesiunii nu trebuie să fie mai mică de 0 + Token: + Invalid: Token-ul de sesiune este invalid + WebAuthN: + NoChallenge: Sesiune fără provocare WebAuthN + Intent: + IDPMissing: ID-ul IDP lipsește în cerere + IDPInvalid: IDP invalid pentru cerere + ResponseInvalid: Răspunsul IDP este invalid + MissingSingleMappingAttribute: Răspunsul IDP nu conține atributul de mapare sau are mai mult de o valoare + SuccessURLMissing: URL-ul de succes lipsește în cerere + FailureURLMissing: URL-ul de eșec lipsește în cerere + StateMissing: Parametrul de stare lipsește în cerere + NotStarted: Intenția nu este pornită sau a fost deja terminată + NotSucceeded: Intenția nu a reușit + TokenCreationFailed: Crearea token-ului a eșuat + InvalidToken: Token-ul intenției este invalid + OtherUser: Intenția este destinată altui utilizator + AuthRequest: + AlreadyExists: Cererea de autentificare există deja + NotExisting: Cererea de autentificare nu există + WrongLoginClient: Cererea de autentificare a fost creată de alt client de autentificare + OIDCSession: + RefreshTokenInvalid: Token-ul de reîmprospătare este invalid + Token: + Invalid: Token-ul este invalid + Expired: Token-ul a expirat + InvalidClient: Token-ul nu a fost emis pentru acest client + SAMLRequest: + AlreadyExists: Cererea SAML există deja + NotExisting: Cererea SAML nu există + WrongLoginClient: Cererea SAML a fost creată de alt client de autentificare + SAMLSession: + InvalidClient: Răspunsul SAML nu a fost emis pentru acest client + Feature: + NotExisting: Caracteristica nu există + TypeNotSupported: Tipul caracteristicii nu este suportat + InvalidValue: Valoare invalidă pentru această caracteristică + Target: + Invalid: Ținta este invalidă + NoTimeout: Ținta nu are timp de așteptare + InvalidURL: Ținta are un URL invalid + NotFound: Ținta nu a fost găsită + Execution: + ConditionInvalid: Condiția de execuție este invalidă + Invalid: Execuția este invalidă + NotFound: Execuția nu a fost găsită + IncludeNotFound: Includerea nu a fost găsită + NoTargets: Nu sunt definite ținte + Failed: Execuția a eșuat + ResponseIsNotValidJSON: Răspunsul nu este un JSON valid + UserSchema: + NotEnabled: Caracteristica "Schema de utilizator" nu este activată + Type: + Missing: Tipul schemei de utilizator lipsește + AlreadyExists: Tipul schemei de utilizator există deja + Authenticator: + Invalid: Tip de autentificator invalid + NotActive: Schema de utilizator nu este activă + NotInactive: Schema de utilizator nu este inactivă + NotExists: Schema de utilizator nu există + ID: + Missing: ID-ul schemei de utilizator lipsește + Invalid: Schema de utilizator este invalidă + Data: + Invalid: Datele sunt invalide pentru schema de utilizator + TokenExchange: + FeatureDisabled: Caracteristica Token Exchange este dezactivată pentru instanța dvs. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features + Token: + Missing: Token-ul lipsește + Invalid: Token-ul este invalid + TypeMissing: Tipul token-ului lipsește + TypeNotAllowed: Tipul token-ului nu este permis + TypeNotSupported: Tipul token-ului nu este suportat + NotForAPI: Token-urile impersonate nu sunt permise pentru API + Impersonation: + PolicyDisabled: Impersonarea este dezactivată în politica de securitate a instanței + WebKey: + ActiveDelete: Nu se poate șterge o cheie web activă + Config: Configurație cheie web invalidă + Duplicate: ID-ul cheii web nu este unic + FeatureDisabled: Caracteristica cheie web este dezactivată + NoActive: Nu a fost găsită nicio cheie web activă + NotFound: Cheia web nu a fost găsită + + AggregateTypes: + action: Acțiune + instance: Instanță + key_pair: Pereche de chei + org: Organizație + project: Proiect + user: Utilizator + usergrant: Acordare de utilizator + quota: Cotă + feature: Caracteristică + target: Țintă + execution: Execuție + user_schema: Schema de utilizator + auth_request: Cerere de autentificare + device_auth: Autentificare dispozitiv + idpintent: Intenție IDP + limits: Limite + milestone: Piatră de hotar + oidc_session: Sesiune OIDC + restrictions: Restricții + system: Sistem + session: Sesiune + web_key: Cheie web + saml_request: Cerere SAML + saml_session: Sesiune SAML + + EventTypes: + execution: + set: Execuție setată + removed: Execuție ștearsă + target: + added: Țintă creată + changed: Țintă schimbată + removed: Țintă ștearsă + user: + added: Utilizator adăugat + selfregistered: Utilizator s-a înregistrat singur + initialization: + code: + added: Cod de inițializare generat + sent: Cod de inițializare trimis + check: + succeeded: Verificarea inițializării a reușit + failed: Verificarea inițializării a eșuat + token: + added: Token de acces creat + v2.added: Token de acces creat + removed: Token de acces șters + impersonated: Utilizator impersonat + username: + reserved: Nume de utilizator rezervat + released: Nume de utilizator eliberat + changed: Nume de utilizator schimbat + email: + reserved: Adresă de e-mail rezervată + released: Adresă de e-mail eliberată + changed: Adresă de e-mail schimbată + verified: Adresă de e-mail verificată + verification: + failed: Verificarea adresei de e-mail a eșuat + code: + added: Cod de verificare a adresei de e-mail generat + sent: Cod de verificare a adresei de e-mail trimis + machine: + added: Utilizator tehnic adăugat + changed: Utilizator tehnic schimbat + key: + added: Cheie adăugată + removed: Cheie ștearsă + secret: + set: Secret setat + updated: Hash secret actualizat + removed: Secret șters + check: + succeeded: Verificarea secretului a reușit + failed: Verificarea secretului a eșuat + human: + added: Persoană adăugată + selfregistered: Persoană s-a înregistrat singură + avatar: + added: Avatar adăugat + removed: Avatar șters + initialization: + code: + added: Cod de inițializare generat + sent: Cod de inițializare trimis + check: + succeeded: Verificarea inițializării a reușit + failed: Verificarea inițializării a eșuat + invite: + code: + added: Cod de invitație generat + sent: Cod de invitație trimis + check: + succeeded: Verificarea invitației a reușit + failed: Verificarea invitației a eșuat + username: + reserved: Nume de utilizator rezervat + released: Nume de utilizator eliberat + email: + changed: Adresă de e-mail schimbată + verified: Adresă de e-mail verificată + verification: + failed: Verificarea adresei de e-mail a eșuat + code: + added: Cod de verificare a adresei de e-mail generat + sent: Cod de verificare a adresei de e-mail trimis + password: + changed: Parolă schimbată + code: + added: Cod de parolă generat + sent: Cod de parolă trimis + check: + succeeded: Verificarea parolei a reușit + failed: Verificarea parolei a eșuat + change: + sent: Schimbare de parolă trimisă + hash: + updated: Hash de parolă actualizat + externallogin: + check: + succeeded: Autentificare externă a reușit + externalidp: + added: IDP extern adăugat + removed: IDP extern șters + cascade: + removed: IDP extern cascadă șters + id: + migrated: UserID extern al IDP a fost migrat + phone: + changed: Număr de telefon schimbat + verified: Număr de telefon verificat + verification: + failed: Verificarea numărului de telefon a eșuat + code: + added: Cod de număr de telefon generat + sent: Cod de număr de telefon trimis + removed: Număr de telefon șters + profile: + changed: Profil de utilizator schimbat + address: + changed: Adresă de utilizator schimbată + mfa: + otp: + added: Multifactor OTP adăugat + verified: Multifactor OTP verificat + removed: Multifactor OTP șters + check: + succeeded: Verificarea Multifactor OTP a reușit + failed: Verificarea Multifactor OTP a eșuat + sms: + added: Multifactor OTP SMS adăugat + removed: Multifactor OTP SMS șters + code: + added: Cod Multifactor OTP SMS adăugat + sent: Cod Multifactor OTP SMS trimis + check: + succeeded: Verificarea Multifactor OTP SMS a reușit + failed: Verificarea Multifactor OTP SMS a eșuat + email: + added: Multifactor OTP Email adăugat + removed: Multifactor OTP Email șters + code: + added: Cod Multifactor OTP Email adăugat + sent: Cod Multifactor OTP Email trimis + check: + succeeded: Verificarea Multifactor OTP Email a reușit + failed: Verificarea Multifactor OTP Email a eșuat + u2f: + token: + added: Token pentru Multifactor U2F adăugat + verified: Token pentru Multifactor U2F verificat + removed: Token pentru Multifactor U2F șters + begin: + login: Verificarea Multifactor U2F a început + check: + succeeded: Verificarea Multifactor U2F a reușit + failed: Verificarea Multifactor U2F a eșuat + signcount: + changed: Suma de control a token-ului Multifactor U2F a fost schimbată + init: + skipped: Inițializarea Multifactor a fost omisă + passwordless: + token: + added: Token pentru conectare fără parolă adăugat + verified: Token pentru conectare fără parolă verificat + removed: Token pentru conectare fără parolă șters + begin: + login: Verificarea conectării fără parolă a început + check: + succeeded: Verificarea conectării fără parolă a reușit + failed: Verificarea conectării fără parolă a eșuat + signcount: + changed: Suma de control a token-ului de conectare fără parolă a fost schimbată + initialization: + code: + added: Cod de inițializare fără parolă adăugat + sent: Cod de inițializare fără parolă trimis + requested: Cod de inițializare fără parolă solicitat + check: + succeeded: Codul de inițializare fără parolă a fost verificat cu succes + failed: Verificarea codului de inițializare fără parolă a eșuat + signed: + out: Utilizator deconectat + refresh: + token: + added: Token de reîmprospătare creat + renewed: Token de reîmprospătare reînnoit + removed: Token de reîmprospătare șters + locked: Utilizator blocat + unlocked: Utilizator deblocat + deactivated: Utilizator dezactivat + reactivated: Utilizator reactivat + removed: Utilizator șters + password: + changed: Parolă schimbată + code: + added: Cod de parolă generat + sent: Cod de parolă trimis + check: + succeeded: Verificarea parolei a reușit + failed: Verificarea parolei a eșuat + phone: + changed: Număr de telefon schimbat + verified: Număr de telefon verificat + verification: + failed: Verificarea numărului de telefon a eșuat + code: + added: Cod de număr de telefon generat + sent: Cod de număr de telefon trimis + + profile: + changed: Profil de utilizator schimbat + address: + changed: Adresă de utilizator schimbată + mfa: + otp: + added: Multifactor OTP adăugat + verified: Multifactor OTP verificat + removed: Multifactor OTP șters + check: + succeeded: Verificarea Multifactor OTP a reușit + failed: Verificarea Multifactor OTP a eșuat + init: + skipped: Inițializarea Multifactor OTP a fost omisă + init: + skipped: Inițializarea Multifactor a fost omisă + signed: + out: Utilizator deconectat + grant: + added: Autorizație adăugată + changed: Autorizație schimbată + removed: Autorizație ștearsă + deactivated: Autorizație dezactivată + reactivated: Autorizație reactivată + reserved: Autorizație rezervată + released: Autorizație eliberată + cascade: + removed: Autorizație ștearsă + changed: Autorizație schimbată + metadata: + set: Metadate de utilizator setate + removed: Metadate de utilizator șterse + removed.all: Toate metadatele de utilizator au fost șterse + domain: + claimed: Domeniu revendicat + claimed.sent: Notificarea de revendicare a domeniului a fost trimisă + pat: + added: Token de acces personal adăugat + removed: Token de acces personal șters + org: + added: Organizație adăugată + changed: Organizație schimbată + deactivated: Organizație dezactivată + reactivated: Organizație reactivată + removed: Organizație ștearsă + domain: + added: Domeniu adăugat + verification: + added: Verificare de domeniu adăugată + failed: Verificare de domeniu eșuată + verified: Domeniu verificat + removed: Domeniu șters + primary: + set: Domeniu principal setat + reserved: Domeniu rezervat + released: Domeniu eliberat + name: + reserved: Nume de organizație rezervat + released: Nume de organizație eliberat + member: + added: Membru al organizației adăugat + changed: Membru al organizației schimbat + removed: Membru al organizației șters + cascade: + removed: Membru al organizației cascadă șters + iam: + policy: + added: Politică de sistem adăugată + changed: Politică de sistem schimbată + removed: Politică de sistem ștearsă + idp: + config: + added: Configurație IDP adăugată + changed: Configurație IDP schimbată + removed: Configurație IDP ștearsă + deactivated: Configurație IDP dezactivată + reactivated: Configurație IDP reactivată + oidc: + config: + added: Configurație OIDC IDP adăugată + changed: Configurație OIDC IDP schimbată + saml: + config: + added: Configurație SAML IDP adăugată + changed: Configurație SAML IDP schimbată + jwt: + config: + added: Configurație JWT IDP adăugată + changed: Configurație JWT IDP schimbată + customtext: + set: Text personalizat setat + removed: Text personalizat șters + template: + removed: Șablon de text personalizat șters + policy: + login: + added: Politică de conectare adăugată + changed: Politică de conectare schimbată + removed: Politică de conectare ștearsă + idpprovider: + added: Furnizor de identitate adăugat la politica de conectare + removed: Furnizor de identitate șters din politica de conectare + cascade: + removed: Furnizor de identitate cascadă șters din politica de conectare + secondfactor: + added: Al doilea factor adăugat la politica de conectare + removed: Al doilea factor șters din politica de conectare + multifactor: + added: Multi-factor adăugat la politica de conectare + removed: Multi-factor șters din politica de conectare + password: + complexity: + added: Politică de complexitate a parolei adăugată + changed: Politică de complexitate a parolei schimbată + removed: Politică de complexitate a parolei ștearsă + age: + added: Politică de vârstă a parolei adăugată + changed: Politică de vârstă a parolei schimbată + removed: Politică de vârstă a parolei ștearsă + lockout: + added: Politică de blocare a parolei adăugată + changed: Politică de blocare a parolei schimbată + removed: Politică de blocare a parolei ștearsă + label: + added: Politică de etichete adăugată + changed: Politică de etichete schimbată + activated: Politică de etichete activată + removed: Politică de etichete ștearsă + logo: + added: Logo adăugat la politica de etichete + removed: Logo șters din politica de etichete + dark: + added: Logo (modul întunecat) adăugat la politica de etichete + removed: Logo (modul întunecat) șters din politica de etichete + icon: + added: Icon adăugat la politica de etichete + removed: Icon șters din politica de etichete + dark: + added: Icon (modul întunecat) adăugat la politica de etichete + removed: Icon (modul întunecat) șters din politica de etichete + font: + added: Font adăugat la politica de etichete + removed: Font șters din politica de etichete + assets: + removed: Active șterse din politica de etichete + privacy: + added: Politică de confidențialitate și TOS adăugate + changed: Politică de confidențialitate și TOS schimbate + removed: Politică de confidențialitate și TOS șterse + domain: + added: Politică de domeniu adăugată + changed: Politică de domeniu schimbată + removed: Politică de domeniu ștearsă + lockout: + added: Politică de blocare adăugată + changed: Politică de blocare schimbată + removed: Politică de blocare ștearsă + notification: + added: Politică de notificare adăugată + changed: Politică de notificare schimbată + removed: Politică de notificare ștearsă + flow: + trigger_actions: + set: Acțiune setată + cascade: + removed: Acțiuni cascadă șterse + removed: Acțiuni șterse + cleared: Flux șters + mail: + template: + added: Șablon de e-mail adăugat + changed: Șablon de e-mail schimbat + removed: Șablon de e-mail șters + text: + added: Text de e-mail adăugat + changed: Text de e-mail schimbat + removed: Text de e-mail șters + metadata: + removed: Metadate șterse + removed.all: Toate metadatele au fost șterse + set: Metadate setate + project: + added: Proiect adăugat + changed: Proiect schimbat + deactivated: Proiect dezactivat + reactivated: Proiect reactivat + removed: Proiect șters + member: + added: Membru al proiectului adăugat + changed: Membru al proiectului schimbat + removed: Membru al proiectului șters + cascade: + removed: Membru al proiectului cascadă șters + role: + added: Rol de proiect adăugat + changed: Rol de proiect schimbat + removed: Rol de proiect șters + grant: + added: Acces de gestionare adăugat + changed: Acces de gestionare schimbat + removed: Acces de gestionare șters + deactivated: Acces de gestionare dezactivat + reactivated: Acces de gestionare reactivat + cascade: + changed: Acces de gestionare schimbat + member: + added: Membru de acces de gestionare adăugat + changed: Membru de acces de gestionare schimbat + removed: Membru de acces de gestionare șters + cascade: + removed: Acces de gestionare cascadă șters + application: + added: Aplicație adăugată + changed: Aplicație schimbată + removed: Aplicație ștearsă + deactivated: Aplicație dezactivată + reactivated: Aplicație reactivată + oidc: + secret: + check: + succeeded: Verificarea secretului clientului OIDC a reușit + failed: Verificarea secretului clientului OIDC a eșuat + key: + added: Cheie de aplicație OIDC adăugată + removed: Cheie de aplicație OIDC ștearsă + api: + secret: + check: + succeeded: Verificarea secretului API a reușit + failed: Verificarea secretului API a eșuat + key: + added: Cheie de aplicație adăugată + removed: Cheie de aplicație ștearsă + config: + saml: + added: Configurație SAML adăugată + changed: Configurație SAML schimbată + oidc: + added: Configurație OIDC adăugată + changed: Configurație OIDC schimbată + secret: + changed: Secret OIDC schimbat + updated: Hash secret OIDC actualizat + api: + added: Configurație API adăugată + changed: Configurație API schimbată + secret: + changed: Secret API schimbat + updated: Hash secret API actualizat + policy: + password: + complexity: + added: Politică de complexitate a parolei adăugată + changed: Politică de complexitate a parolei schimbată + age: + added: Politică de vârstă a parolei adăugată + changed: Politică de vârstă a parolei schimbată + lockout: + added: Politică de blocare a parolei adăugată + changed: Politică de blocare a parolei schimbată + iam: + setup: + started: Configurarea ZITADEL a început + done: Configurarea ZITADEL a fost finalizată + global: + org: + set: Organizație globală setată + project: + iam: + set: Proiect ZITADEL setat + member: + added: Membru ZITADEL adăugat + changed: Membru ZITADEL schimbat + removed: Membru ZITADEL șters + cascade: + removed: Membru ZITADEL cascadă șters + idp: + config: + added: Configurație IDP adăugată + changed: Configurație IDP schimbată + removed: Configurație IDP ștearsă + deactivated: Configurație IDP dezactivată + reactivated: Configurație IDP reactivată + oidc: + config: + added: Configurație OIDC IDP adăugată + changed: Configurație OIDC IDP schimbată + saml: + config: + added: Configurație SAML IDP adăugată + changed: Configurație SAML IDP schimbată + jwt: + config: + added: Configurație JWT adăugată la furnizorul de identitate + changed: Configurație JWT ștearsă din furnizorul de identitate + customtext: + set: Text a fost setat + removed: Text a fost șters + policy: + login: + added: Politică de conectare implicită adăugată + changed: Politică de conectare implicită schimbată + idpprovider: + added: Furnizor de identitate adăugat la politica de conectare implicită + removed: Furnizor de identitate șters din politica de conectare implicită + label: + added: Politică de etichete adăugată + changed: Politică de etichete schimbată + activated: Politică de etichete activată + logo: + added: Logo adăugat la politica de etichete + removed: Logo șters din politica de etichete + dark: + added: Logo (modul întunecat) adăugat la politica de etichete + removed: Logo (modul întunecat) șters din politica de etichete + icon: + added: Icon adăugat la politica de etichete + removed: Icon șters din politica de etichete + dark: + added: Icon (modul întunecat) adăugat la politica de etichete + removed: Icon (modul întunecat) șters din politica de etichete + font: + added: Font adăugat la politica de etichete + removed: Font șters din politica de etichete + assets: + removed: Active șterse din politica de etichete + default: + language: + set: Limba implicită setată + oidc: + settings: + added: Configurație OIDC adăugată + changed: Configurație OIDC schimbată + removed: Configurație OIDC ștearsă + secret: + generator: + added: Generator de secrete adăugat + changed: Generator de secrete schimbat + removed: Generator de secrete șters + smtp: + config: + added: Configurație SMTP adăugată + changed: Configurație SMTP schimbată + activated: Configurație SMTP activată + deactivated: Configurație SMTP dezactivată + removed: Configurație SMTP ștearsă + password: + changed: Secretul configurației SMTP a fost schimbat + sms: + config: + twilio: + added: Furnizor SMS Twilio adăugat + changed: Furnizor SMS Twilio schimbat + token: + changed: Token furnizor SMS Twilio schimbat + removed: Furnizor SMS Twilio șters + activated: Furnizor SMS Twilio activat + deactivated: Furnizor SMS Twilio dezactivat + key_pair: + added: Pereche de chei adăugată + certificate: + added: Certificat adăugat + action: + added: Acțiune adăugată + changed: Acțiune schimbată + deactivated: Acțiune dezactivată + reactivated: Acțiune reactivată + removed: Acțiune ștearsă + instance: + added: Instanță adăugată + changed: Instanță schimbată + customtext: + removed: Text personalizat șters + set: Text personalizat setat + template: + removed: Șablon de text personalizat șters + default: + language: + set: Limba implicită setată + org: + set: Organizația implicită setată + domain: + added: Domeniu adăugat + primary: + set: Domeniu principal setat + removed: Domeniu șters + iam: + console: + set: Aplicația ZITADEL Console setată + project: + set: Proiectul ZITADEL setat + mail: + template: + added: Șablon de e-mail adăugat + changed: Șablon de e-mail schimbat + text: + added: Text de e-mail adăugat + changed: Text de e-mail schimbat + member: + added: Membru al instanței adăugat + changed: Membru al instanței schimbat + removed: Membru al instanței șters + cascade: + removed: Membru al instanței cascadă șters + notification: + provider: + debug: + fileadded: Furnizor de notificări de depanare a fișierelor adăugat + filechanged: Furnizor de notificări de depanare a fișierelor schimbat + fileremoved: Furnizor de notificări de depanare a fișierelor șters + logadded: Furnizor de notificări de depanare a jurnalelor adăugat + logchanged: Furnizor de notificări de depanare a jurnalelor schimbat + logremoved: Furnizor de notificări de depanare a jurnalelor șters + oidc: + settings: + added: Setări OIDC adăugate + changed: Setări OIDC schimbate + policy: + domain: + added: Politică de domeniu adăugată + changed: Politică de domeniu schimbată + label: + activated: Politică de etichete activată + added: Politică de etichete adăugată + assets: + removed: Activ șters din politica de etichete + changed: Politică de etichete schimbată + font: + added: Font adăugat la politica de etichete + removed: Font șters din politica de etichete + icon: + added: Icon adăugat la politica de etichete + removed: Icon șters din politica de etichete + dark: + added: Icon adăugat la politica de etichete întunecate + removed: Icon șters din politica de etichete întunecate + logo: + added: Logo adăugat la politica de etichete + removed: Logo șters din politica de etichete + dark: + added: Logo adăugat la politica de etichete întunecate + removed: Logo șters din politica de etichete întunecate + lockout: + added: Politică de blocare adăugată + changed: Politică de blocare schimbată + login: + added: Politică de conectare adăugată + changed: Politică de conectare schimbată + idpprovider: + added: Furnizor de identitate adăugat la politica de conectare + cascade: + removed: Furnizor de identitate cascadă șters din politica de conectare + removed: Furnizor de identitate șters din politica de conectare + multifactor: + added: Multifactor adăugat la politica de conectare + removed: Multifactor șters din politica de conectare + secondfactor: + added: Al doilea factor adăugat la politica de conectare + removed: Al doilea factor șters din politica de conectare + password: + age: + added: Politică de vârstă a parolei adăugată + changed: Politică de vârstă a parolei schimbată + complexity: + added: Politică de complexitate a parolei adăugată + changed: Politică de complexitate a parolei schimbată + privacy: + added: Politică de confidențialitate adăugată + changed: Politică de confidențialitate schimbată + security: + set: Politică de securitate setată + + removed: Instanță ștearsă + secret: + generator: + added: Generator de secrete adăugat + changed: Generator de secrete schimbat + removed: Generator de secrete șters + sms: + configtwilio: + activated: Configurație Twilio SMS activată + added: Configurație Twilio SMS adăugată + changed: Configurație Twilio SMS schimbată + deactivated: Configurație Twilio SMS dezactivată + removed: Configurație Twilio SMS ștearsă + token: + changed: Token al configurației Twilio SMS schimbat + smtp: + config: + added: Configurație SMTP adăugată + changed: Configurație SMTP schimbată + activated: Configurație SMTP activată + deactivated: Configurație SMTP dezactivată + password: + changed: Parolă a configurației SMTP schimbată + removed: Configurație SMTP ștearsă + user_schema: + created: Schemă de utilizator creată + updated: Schemă de utilizator actualizată + deactivated: Schemă de utilizator dezactivată + reactivated: Schemă de utilizator reactivată + deleted: Schemă de utilizator ștearsă + user: + created: Utilizator creat + updated: Utilizator actualizat + deleted: Utilizator șters + email: + updated: Adresă de e-mail schimbată + verified: Adresă de e-mail verificată + verification: + failed: Verificarea adresei de e-mail a eșuat + code: + added: Cod de verificare a adresei de e-mail generat + sent: Cod de verificare a adresei de e-mail trimis + phone: + updated: Număr de telefon schimbat + verified: Număr de telefon verificat + verification: + failed: Verificarea numărului de telefon a eșuat + code: + added: Cod de număr de telefon generat + sent: Cod de număr de telefon trimis + + web_key: + added: Cheie Web adăugată + activated: Cheie Web activată + deactivated: Cheie Web dezactivată + removed: Cheie Web ștearsă + + Application: + OIDC: + UnsupportedVersion: Versiunea dvs. OIDC nu este suportată + V1: + NotCompliant: Configurația dvs. nu este conformă și diferă de standardul OIDC 1.0. + NoRedirectUris: Trebuie înregistrat cel puțin un URI de redirecționare. + NotAllCombinationsAreAllowed: Configurația este conformă, dar nu toate combinațiile posibile sunt permise. + Code: + RedirectUris: + HttpOnlyForWeb: Tipul de acordare code permite doar URI-uri de redirecționare http pentru tipul de aplicație web. + CustomOnlyForNative: Tipul de acordare code permite doar URI-uri de redirecționare personalizate pentru tipul de aplicație nativă (de exemplu, appname://) + Implicit: + RedirectUris: + CustomNotAllowed: Tipul de acordare implicit nu permite URI-uri de redirecționare personalizate + HttpNotAllowed: Tipul de acordare implicit nu permite URI-uri de redirecționare http + HttpLocalhostOnlyForNative: URI-ul de redirecționare Http://localhost este permis doar pentru aplicațiile native. + Native: + AuthMethodType: + NotNone: Aplicațiile native ar trebui să aibă authmethodtype none. + RedirectUris: + MustBeHttpLocalhost: URI-urile de redirecționare trebuie să înceapă cu propriul dvs. protocol, http://127.0.0.1, http://[::1] sau http://localhost. + UserAgent: + AuthMethodType: + NotNone: Aplicația agentului utilizator ar trebui să aibă authmethodtype none. + GrantType: + Refresh: + NoAuthCode: Token-ul de reîmprospătare este permis doar în combinație cu codul de autorizare. + + Action: + Flow: + Type: + Unspecified: Nespecificat + ExternalAuthentication: Autentificare externă + CustomiseToken: Completare Token + InternalAuthentication: Autentificare internă + CustomizeSAMLResponse: Completează SAMLResponse + TriggerType: + Unspecified: Nespecificat + PostAuthentication: Post Autentificare + PreCreation: Pre Creare + PostCreation: Post Creare + PreUserinfoCreation: Pre Creare Userinfo + PreAccessTokenCreation: Pre Creare Token de Acces + PreSAMLResponseCreation: Pre Creare Răspuns SAML diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 8c4b079f2e..64a8ef8013 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -532,6 +532,7 @@ Errors: AlreadyExists: Запрос на аутентификацию уже существует NotExisting: Запрос на аутентификацию не существует WrongLoginClient: Запрос на аутентификацию, созданный другим клиентом входа + AlreadyHandled: Запрос аутентификации уже обработан OIDCSession: RefreshTokenInvalid: Маркер обновления недействителен Token: @@ -542,8 +543,12 @@ Errors: AlreadyExists: SAMLRequest уже существует NotExisting: SAMLRequest не существует WrongLoginClient: SAMLRequest создан другим клиентом входа + AlreadyHandled: Запрос SAML уже обработан SAMLSession: InvalidClient: SAMLResponse не был отправлен для этого клиента + DeviceAuth: + NotFound: Запрос авторизации устройства не существует + AlreadyHandled: Запрос авторизации устройства уже обработан Feature: NotExisting: ункция не существует TypeNotSupported: Тип объекта не поддерживается diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index e31095b78c..2c292976d3 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: Autentiseringsbegäran finns redan NotExisting: Autentiseringsbegäran existerar inte WrongLoginClient: Autentiseringsbegäran skapad av annan inloggningsklient + AlreadyHandled: Autentiseringsbegäran har redan hanterats OIDCSession: RefreshTokenInvalid: Uppdateringstoken är ogiltig Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest finns redan NotExisting: SAMLRequest finns inte WrongLoginClient: SAMLRequest skapad av annan inloggningsklient + AlreadyHandled: SAML-begäran har redan hanterats SAMLSession: InvalidClient: SAMLResponse utfärdades inte för den här klienten + DeviceAuth: + NotFound: Begäran om enhetsauktorisering finns inte + AlreadyHandled: Begäran om enhetsauktorisering har redan hanterats Feature: NotExisting: Funktionen existerar inte TypeNotSupported: Funktionstypen stöds inte diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index b30609da90..d4b36df7ff 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -543,6 +543,7 @@ Errors: AlreadyExists: AuthRequest已经存在 NotExisting: AuthRequest不存在 WrongLoginClient: 其他登录客户端创建的AuthRequest + AlreadyHandled: 身份验证请求已被处理 OIDCSession: RefreshTokenInvalid: Refresh Token 无效 Token: @@ -553,8 +554,12 @@ Errors: AlreadyExists: SAMLRequest 已存在 NotExisting: SAMLRequest不存在 WrongLoginClient: 其他登录客户端创建的 SAMLRequest + AlreadyHandled: SAML请求已被处理 SAMLSession: InvalidClient: 未向该客户端发出 SAMLResponse + DeviceAuth: + NotFound: 设备授权请求不存在 + AlreadyHandled: 设备授权请求已被处理 Feature: NotExisting: 功能不存在 TypeNotSupported: 不支持功能类型 diff --git a/internal/telemetry/metrics/config/config.go b/internal/telemetry/metrics/config/config.go index e9bcbe45c2..9e9ebec52b 100644 --- a/internal/telemetry/metrics/config/config.go +++ b/internal/telemetry/metrics/config/config.go @@ -1,6 +1,7 @@ package config import ( + "github.com/zitadel/zitadel/internal/telemetry/metrics" "github.com/zitadel/zitadel/internal/telemetry/metrics/otel" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -12,11 +13,16 @@ type Config struct { var meter = map[string]func(map[string]interface{}) error{ "otel": otel.NewTracerFromConfig, - "none": NoMetrics, - "": NoMetrics, + "none": registerNoopMetrics, + "": registerNoopMetrics, } func (c *Config) NewMeter() error { + // When using start-from-init or start-from-setup the metric provider + // was already set in the setup phase and the start phase must not overwrite it. + if metrics.M != nil { + return nil + } t, ok := meter[c.Type] if !ok { return zerrors.ThrowInternalf(nil, "METER-Dfqsx", "config type %s not supported", c.Type) @@ -25,6 +31,7 @@ func (c *Config) NewMeter() error { return t(c.Config) } -func NoMetrics(_ map[string]interface{}) error { +func registerNoopMetrics(rawConfig map[string]interface{}) (err error) { + metrics.M = &metrics.NoopMetrics{} return nil } diff --git a/internal/telemetry/metrics/metrics.go b/internal/telemetry/metrics/metrics.go index 503ebc22de..b25dc619c0 100644 --- a/internal/telemetry/metrics/metrics.go +++ b/internal/telemetry/metrics/metrics.go @@ -22,8 +22,10 @@ type Metrics interface { GetMetricsProvider() metric.MeterProvider RegisterCounter(name, description string) error AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error + AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error + RegisterHistogram(name, description, unit string, buckets []float64) error } var M Metrics @@ -56,6 +58,20 @@ func AddCount(ctx context.Context, name string, value int64, labels map[string]a return M.AddCount(ctx, name, value, labels) } +func AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + if M == nil { + return nil + } + return M.AddHistogramMeasurement(ctx, name, value, labels) +} + +func RegisterHistogram(name, description, unit string, buckets []float64) error { + if M == nil { + return nil + } + return M.RegisterHistogram(name, description, unit, buckets) +} + func RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { if M == nil { return nil diff --git a/internal/telemetry/metrics/mock.go b/internal/telemetry/metrics/mock.go new file mode 100644 index 0000000000..b28a6f5d40 --- /dev/null +++ b/internal/telemetry/metrics/mock.go @@ -0,0 +1,95 @@ +package metrics + +import ( + "context" + "net/http" + "sync" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +// MockMetrics implements the metrics.Metrics interface for testing +type MockMetrics struct { + mu sync.RWMutex + histogramValues map[string][]float64 + counterValues map[string]int64 + histogramLabels map[string][]map[string]attribute.Value + counterLabels map[string][]map[string]attribute.Value +} + +var _ Metrics = new(MockMetrics) + +// NewMockMetrics creates a new Metrics instance for testing +func NewMockMetrics() *MockMetrics { + return &MockMetrics{ + histogramValues: make(map[string][]float64), + counterValues: make(map[string]int64), + histogramLabels: make(map[string][]map[string]attribute.Value), + counterLabels: make(map[string][]map[string]attribute.Value), + } +} + +func (m *MockMetrics) GetExporter() http.Handler { + return nil +} + +func (m *MockMetrics) GetMetricsProvider() metric.MeterProvider { + return nil +} + +func (m *MockMetrics) RegisterCounter(name, description string) error { + return nil +} + +func (m *MockMetrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error { + m.mu.Lock() + defer m.mu.Unlock() + m.counterValues[name] += value + m.counterLabels[name] = append(m.counterLabels[name], labels) + return nil +} + +func (m *MockMetrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + m.mu.Lock() + defer m.mu.Unlock() + m.histogramValues[name] = append(m.histogramValues[name], value) + m.histogramLabels[name] = append(m.histogramLabels[name], labels) + return nil +} + +func (m *MockMetrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { + return nil +} + +func (m *MockMetrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error { + return nil +} + +func (m *MockMetrics) RegisterHistogram(name, description, unit string, buckets []float64) error { + return nil +} + +func (m *MockMetrics) GetHistogramValues(name string) []float64 { + m.mu.RLock() + defer m.mu.RUnlock() + return m.histogramValues[name] +} + +func (m *MockMetrics) GetHistogramLabels(name string) []map[string]attribute.Value { + m.mu.RLock() + defer m.mu.RUnlock() + return m.histogramLabels[name] +} + +func (m *MockMetrics) GetCounterValue(name string) int64 { + m.mu.RLock() + defer m.mu.RUnlock() + return m.counterValues[name] +} + +func (m *MockMetrics) GetCounterLabels(name string) []map[string]attribute.Value { + m.mu.RLock() + defer m.mu.RUnlock() + return m.counterLabels[name] +} diff --git a/internal/telemetry/metrics/noop.go b/internal/telemetry/metrics/noop.go new file mode 100644 index 0000000000..954db1d2b9 --- /dev/null +++ b/internal/telemetry/metrics/noop.go @@ -0,0 +1,45 @@ +package metrics + +import ( + "context" + "net/http" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +type NoopMetrics struct{} + +var _ Metrics = new(NoopMetrics) + +func (n *NoopMetrics) GetExporter() http.Handler { + return nil +} + +func (n *NoopMetrics) GetMetricsProvider() metric.MeterProvider { + return nil +} + +func (n *NoopMetrics) RegisterCounter(name, description string) error { + return nil +} + +func (n *NoopMetrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error { + return nil +} + +func (n *NoopMetrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + return nil +} + +func (n *NoopMetrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { + return nil +} + +func (n *NoopMetrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error { + return nil +} + +func (n *NoopMetrics) RegisterHistogram(name, description, unit string, buckets []float64) error { + return nil +} diff --git a/internal/telemetry/metrics/otel/open_telemetry.go b/internal/telemetry/metrics/otel/open_telemetry.go index 21a45699f1..c4509ed5db 100644 --- a/internal/telemetry/metrics/otel/open_telemetry.go +++ b/internal/telemetry/metrics/otel/open_telemetry.go @@ -24,10 +24,11 @@ type Metrics struct { Counters sync.Map UpDownSumObserver sync.Map ValueObservers sync.Map + Histograms sync.Map } func NewMetrics(meterName string) (metrics.Metrics, error) { - resource, err := otel_resource.ResourceWithService() + resource, err := otel_resource.ResourceWithService("ZITADEL") if err != nil { return nil, err } @@ -84,6 +85,33 @@ func (m *Metrics) AddCount(ctx context.Context, name string, value int64, labels return nil } +func (m *Metrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + histogram, exists := m.Histograms.Load(name) + if !exists { + return zerrors.ThrowNotFound(nil, "METER-5wwb1", "Errors.Metrics.Histogram.NotFound") + } + histogram.(metric.Float64Histogram).Record(ctx, value, MapToRecordOption(labels)...) + return nil +} + +func (m *Metrics) RegisterHistogram(name, description, unit string, buckets []float64) error { + if _, exists := m.Histograms.Load(name); exists { + return nil + } + + histogram, err := m.Meter.Float64Histogram(name, + metric.WithDescription(description), + metric.WithUnit(unit), + metric.WithExplicitBucketBoundaries(buckets...), + ) + if err != nil { + return err + } + + m.Histograms.Store(name, histogram) + return nil +} + func (m *Metrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { if _, exists := m.UpDownSumObserver.Load(name); exists { return nil @@ -113,15 +141,23 @@ func (m *Metrics) RegisterValueObserver(name, description string, callbackFunc m } func MapToAddOption(labels map[string]attribute.Value) []metric.AddOption { + return []metric.AddOption{metric.WithAttributes(labelsToAttributes(labels)...)} +} + +func MapToRecordOption(labels map[string]attribute.Value) []metric.RecordOption { + return []metric.RecordOption{metric.WithAttributes(labelsToAttributes(labels)...)} +} + +func labelsToAttributes(labels map[string]attribute.Value) []attribute.KeyValue { if labels == nil { return nil } - keyValues := make([]attribute.KeyValue, 0, len(labels)) + attributes := make([]attribute.KeyValue, 0, len(labels)) for key, value := range labels { - keyValues = append(keyValues, attribute.KeyValue{ + attributes = append(attributes, attribute.KeyValue{ Key: attribute.Key(key), Value: value, }) } - return []metric.AddOption{metric.WithAttributes(keyValues...)} + return attributes } diff --git a/internal/telemetry/otel/resource.go b/internal/telemetry/otel/resource.go index 7a3f249191..c43315e24f 100644 --- a/internal/telemetry/otel/resource.go +++ b/internal/telemetry/otel/resource.go @@ -8,9 +8,9 @@ import ( "github.com/zitadel/zitadel/cmd/build" ) -func ResourceWithService() (*resource.Resource, error) { +func ResourceWithService(serviceName string) (*resource.Resource, error) { attributes := []attribute.KeyValue{ - semconv.ServiceNameKey.String("ZITADEL"), + semconv.ServiceNameKey.String(serviceName), } if build.Version() != "" { attributes = append(attributes, semconv.ServiceVersionKey.String(build.Version())) diff --git a/internal/telemetry/tracing/google/google_tracer.go b/internal/telemetry/tracing/google/google_tracer.go index 8d15bb18fb..5e8008bace 100644 --- a/internal/telemetry/tracing/google/google_tracer.go +++ b/internal/telemetry/tracing/google/google_tracer.go @@ -9,13 +9,15 @@ import ( ) type Config struct { - ProjectID string - Fraction float64 + ProjectID string + Fraction float64 + ServiceName string } func NewTracer(rawConfig map[string]interface{}) (err error) { c := new(Config) c.ProjectID, _ = rawConfig["projectid"].(string) + c.ServiceName, _ = rawConfig["servicename"].(string) c.Fraction, err = otel.FractionFromConfig(rawConfig["fraction"]) if err != nil { return err @@ -34,6 +36,6 @@ func (c *Config) NewTracer() error { return err } - tracing.T, err = otel.NewTracer(sampler, exporter) + tracing.T, err = otel.NewTracer(sampler, exporter, c.ServiceName) return err } diff --git a/internal/telemetry/tracing/log/config.go b/internal/telemetry/tracing/log/config.go index 862e14624c..9713c64622 100644 --- a/internal/telemetry/tracing/log/config.go +++ b/internal/telemetry/tracing/log/config.go @@ -9,12 +9,14 @@ import ( ) type Config struct { - Fraction float64 + Fraction float64 + ServiceName string } func NewTracer(rawConfig map[string]interface{}) (err error) { c := new(Config) c.Fraction, err = otel.FractionFromConfig(rawConfig["fraction"]) + c.ServiceName, _ = rawConfig["servicename"].(string) if err != nil { return err } @@ -32,6 +34,6 @@ func (c *Config) NewTracer() error { return err } - tracing.T, err = otel.NewTracer(sampler, exporter) + tracing.T, err = otel.NewTracer(sampler, exporter, c.ServiceName) return err } diff --git a/internal/telemetry/tracing/otel/config.go b/internal/telemetry/tracing/otel/config.go index 5b417359b9..ee9a7b4aa1 100644 --- a/internal/telemetry/tracing/otel/config.go +++ b/internal/telemetry/tracing/otel/config.go @@ -13,13 +13,15 @@ import ( ) type Config struct { - Fraction float64 - Endpoint string + Fraction float64 + Endpoint string + ServiceName string } func NewTracerFromConfig(rawConfig map[string]interface{}) (err error) { c := new(Config) c.Endpoint, _ = rawConfig["endpoint"].(string) + c.ServiceName, _ = rawConfig["servicename"].(string) c.Fraction, err = FractionFromConfig(rawConfig["fraction"]) if err != nil { return err @@ -54,7 +56,7 @@ func (c *Config) NewTracer() error { return err } - tracing.T, err = NewTracer(sampler, exporter) + tracing.T, err = NewTracer(sampler, exporter, c.ServiceName) return err } diff --git a/internal/telemetry/tracing/otel/open_telemetry.go b/internal/telemetry/tracing/otel/open_telemetry.go index 4a3f137859..1318c2efed 100644 --- a/internal/telemetry/tracing/otel/open_telemetry.go +++ b/internal/telemetry/tracing/otel/open_telemetry.go @@ -18,8 +18,8 @@ type Tracer struct { sampler sdk_trace.Sampler } -func NewTracer(sampler sdk_trace.Sampler, exporter sdk_trace.SpanExporter) (*Tracer, error) { - resource, err := otel_resource.ResourceWithService() +func NewTracer(sampler sdk_trace.Sampler, exporter sdk_trace.SpanExporter, serviceName string) (*Tracer, error) { + resource, err := otel_resource.ResourceWithService(serviceName) if err != nil { return nil, err } diff --git a/internal/v2/eventstore/postgres/push.go b/internal/v2/eventstore/postgres/push.go index 09f663a086..bde74687c7 100644 --- a/internal/v2/eventstore/postgres/push.go +++ b/internal/v2/eventstore/postgres/push.go @@ -171,8 +171,7 @@ func (s *Storage) push(ctx context.Context, tx *sql.Tx, reducer eventstore.Reduc cmd.position.InPositionOrder, ) - stmt.WriteString(s.pushPositionStmt) - stmt.WriteString(`)`) + stmt.WriteString(", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp()))") } stmt.WriteString(` RETURNING created_at, "position"`) diff --git a/internal/v2/eventstore/postgres/push_test.go b/internal/v2/eventstore/postgres/push_test.go index 91fdc1fcd7..bb3254427c 100644 --- a/internal/v2/eventstore/postgres/push_test.go +++ b/internal/v2/eventstore/postgres/push_test.go @@ -1288,7 +1288,6 @@ func Test_push(t *testing.T) { }, }, } - initPushStmt("postgres") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dbMock := mock.NewSQLMock(t, append([]mock.Expectation{mock.ExpectBegin(nil)}, tt.args.expectations...)...) @@ -1297,9 +1296,7 @@ func Test_push(t *testing.T) { t.Errorf("unexpected error in begin: %v", err) t.FailNow() } - s := Storage{ - pushPositionStmt: initPushStmt("postgres"), - } + s := Storage{} err = s.push(context.Background(), tx, tt.args.reducer, tt.args.commands) tt.want.assertErr(t, err) dbMock.Assert(t) diff --git a/internal/v2/eventstore/postgres/storage.go b/internal/v2/eventstore/postgres/storage.go index 3a703a7d17..d4148f4f1a 100644 --- a/internal/v2/eventstore/postgres/storage.go +++ b/internal/v2/eventstore/postgres/storage.go @@ -3,8 +3,6 @@ package postgres import ( "context" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/v2/eventstore" ) @@ -15,9 +13,8 @@ var ( ) type Storage struct { - client *database.DB - config *Config - pushPositionStmt string + client *database.DB + config *Config } type Config struct { @@ -25,23 +22,9 @@ type Config struct { } func New(client *database.DB, config *Config) *Storage { - initPushStmt(client.Type()) return &Storage{ - client: client, - config: config, - pushPositionStmt: initPushStmt(client.Type()), - } -} - -func initPushStmt(typ string) string { - switch typ { - case "cockroach": - return ", hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp()" - case "postgres": - return ", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp())" - default: - logging.WithFields("database_type", typ).Panic("position statement for type not implemented") - return "" + client: client, + config: config, } } diff --git a/load-test/src/oidc.ts b/load-test/src/oidc.ts index edd451ebd6..daccc0e05a 100644 --- a/load-test/src/oidc.ts +++ b/load-test/src/oidc.ts @@ -168,7 +168,7 @@ export async function token(request: TokenRequest): Promise { }); } -const authRequestBiIDTrend = new Trend('oidc_auth_request_by_id_duration', true); +const authRequestByIDTrend = new Trend('oidc_auth_request_by_id_duration', true); export async function authRequestByID(id: string, tokens: any): Promise { const response = http.get(url(`/v2/oidc/auth_requests/${id}`), { headers: { @@ -178,11 +178,11 @@ export async function authRequestByID(id: string, tokens: any): Promise r.status == 200 || fail(`auth request by failed: ${JSON.stringify(r)}`), }); - authRequestBiIDTrend.add(response.timings.duration); + authRequestByIDTrend.add(response.timings.duration); return response; } -const finalizeAuthRequestTrend = new Trend('oidc_auth_requst_by_id_duration', true); +const finalizeAuthRequestTrend = new Trend('oidc_auth_request_finalize', true); export async function finalizeAuthRequest(id: string, session: any, tokens: any): Promise { const res = await http.post( url(`/v2/oidc/auth_requests/${id}`), diff --git a/proto/buf.yaml b/proto/buf.yaml index f8cf192a95..31bc7b4ccc 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -7,6 +7,10 @@ deps: breaking: use: - FILE + - FIELD_NO_DELETE_UNLESS_NAME_RESERVED + - FIELD_NO_DELETE_UNLESS_NUMBER_RESERVED + except: + - FIELD_NO_DELETE ignore_unstable_packages: true lint: use: diff --git a/proto/zitadel/action/v2beta/action_service.proto b/proto/zitadel/action/v2beta/action_service.proto new file mode 100644 index 0000000000..f225905225 --- /dev/null +++ b/proto/zitadel/action/v2beta/action_service.proto @@ -0,0 +1,725 @@ +syntax = "proto3"; + +package zitadel.action.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "zitadel/action/v2beta/target.proto"; +import "zitadel/action/v2beta/execution.proto"; +import "zitadel/action/v2beta/query.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Action Service"; + version: "2.0-beta"; + description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. This service is in beta state. It can AND will continue breaking until a stable version is released."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage custom executions. +// The service provides methods to create, update, delete and list targets and executions. +service ActionService { + + // Create Target + // + // Create a new target to your endpoint, which can be used in executions. + // + // Required permission: + // - `action.target.write` + // + // Required feature flag: + // - `actions` + rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { + option (google.api.http) = { + post: "/v2beta/actions/targets" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The target to create already exists."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Update Target + // + // Update an existing target. + // To generate a new signing key set the optional expirationSigningKey. + // + // Required permission: + // - `action.target.write` + // + // Required feature flag: + // - `actions` + rpc UpdateTarget (UpdateTargetRequest) returns (UpdateTargetResponse) { + option (google.api.http) = { + post: "/v2beta/actions/targets/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The target to update does not exist."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Delete Target + // + // Delete an existing target. This will remove it from any configured execution as well. + // In case the target is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `action.target.delete` + // + // Required feature flag: + // - `actions` + rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { + option (google.api.http) = { + delete: "/v2beta/actions/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target deleted successfully"; + }; + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Get Target + // + // Returns the target identified by the requested ID. + // + // Required permission: + // - `action.target.read` + // + // Required feature flag: + // - `actions` + rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) { + option (google.api.http) = { + get: "/v2beta/actions/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Target retrieved successfully"; + } + }; + responses: { + key: "404" + value: { + description: "The target to update does not exist."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // List targets + // + // List all matching targets. By default all targets of the instance are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `action.target.read` + // + // Required feature flag: + // - `actions` + rpc ListTargets (ListTargetsRequest) returns (ListTargetsResponse) { + option (google.api.http) = { + post: "/v2beta/actions/targets/_search", + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all targets matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Set Execution + // + // Sets an execution to call a target or include the targets of another execution. + // Setting an empty list of targets will remove all targets from the execution, making it a noop. + // + // Required permission: + // - `action.execution.write` + // + // Required feature flag: + // - `actions` + rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { + option (google.api.http) = { + put: "/v2beta/actions/executions" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Execution successfully updated or left unchanged"; + }; + }; + responses: { + key: "400" + value: { + description: "Condition to set execution does not exist or the feature flag `actions` is not enabled."; + } + }; + }; + } + + // List Executions + // + // List all matching executions. By default all executions of the instance are returned that have at least one execution target. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `action.execution.read` + // + // Required feature flag: + // - `actions` + rpc ListExecutions (ListExecutionsRequest) returns (ListExecutionsResponse) { + option (google.api.http) = { + post: "/v2beta/actions/executions/_search" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all non noop executions matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "Invalid list query or the feature flag `actions` is not enabled."; + }; + }; + }; + } + + // List Execution Functions + // + // List all available functions which can be used as condition for executions. + rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { + option (google.api.http) = { + get: "/v2beta/actions/executions/functions" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all functions successfully"; + }; + }; + }; + } + + // List Execution Methods + // + // List all available methods which can be used as condition for executions. + rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { + option (google.api.http) = { + get: "/v2beta/actions/executions/methods" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all methods successfully"; + }; + }; + }; + } + + // List Execution Services + // + // List all available services which can be used as condition for executions. + rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { + option (google.api.http) = { + get: "/v2beta/actions/executions/services" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all services successfully"; + }; + }; + }; + } +} + +message CreateTargetRequest { + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + min_length: 1 + max_length: 1000 + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + option (validate.required) = true; + // Wait for response but response body is ignored, status is checked, call is sent as post. + RESTWebhook rest_webhook = 2; + // Wait for response and response body is used, status is checked, call is sent as post. + RESTCall rest_call = 3; + // Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. + RESTAsync rest_async = 4; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, then the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + google.protobuf.Duration timeout = 5 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + string endpoint = 6 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + min_length: 1 + max_length: 1000 + } + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\": \"ip_allow_list\",\"restWebhook\":{\"interruptOnError\":true},\"timeout\":\"10s\",\"endpoint\":\"https://example.com/hooks/ip_check\"}"; + }; +} + +message CreateTargetResponse { + // The unique identifier of the newly created target. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the target creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Key used to sign and check payload sent to the target. + string signing_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message UpdateTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + optional string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\"" + min_length: 1 + max_length: 1000 + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + // Wait for response but response body is ignored, status is checked, call is sent as post. + RESTWebhook rest_webhook = 3; + // Wait for response and response body is used, status is checked, call is sent as post. + RESTCall rest_call = 4; + // Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. + RESTAsync rest_async = 5; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, then the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + optional google.protobuf.Duration timeout = 6 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + optional string endpoint = 7 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + min_length: 1 + max_length: 1000 + } + ]; + // Regenerate the key used for signing and checking the payload sent to the target. + // Set the graceful period for the existing key. During that time, the previous + // signing key and the new one will be used to sign the request to allow you a smooth + // transition onf your API. + // + // Note that we currently only allow an immediate rotation ("0s") and will support + // longer expirations in the future. + optional google.protobuf.Duration expiration_signing_key = 8 [ + (validate.rules).duration = {const: {seconds: 0, nanos: 0}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"0s\"" + minimum: 0 + maximum: 0 + } + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\": \"ip_allow_list\",\"restCall\":{\"interruptOnError\":true},\"timeout\":\"10s\",\"endpoint\":\"https://example.com/hooks/ip_check\",\"expirationSigningKey\":\"0s\"}"; + }; +} + +message UpdateTargetResponse { + // The timestamp of the change of the target. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Key used to sign and check payload sent to the target. + optional string signing_key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message DeleteTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteTargetResponse { + // The timestamp of the deletion of the target. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message GetTargetResponse { + Target target = 1; +} + +message ListTargetsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional TargetFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated TargetSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"TARGET_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"targetNameFilter\":{\"targetName\":\"ip_allow_list\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inTargetIdsFilter\":{\"targetIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListTargetsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Target result = 2; +} + +message SetExecutionRequest { + // Condition defining when the execution should be used. + Condition condition = 1; + // Ordered list of targets called during the execution. + repeated string targets = 2; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"condition\":{\"request\":{\"method\":\"zitadel.session.v2.SessionService/ListSessions\"}},\"targets\":[{\"target\":\"69629026806489455\"}]}"; + }; +} + +message SetExecutionResponse { + // The timestamp of the execution set. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListExecutionsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional ExecutionFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"EXECUTION_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ExecutionSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"EXECUTION_FIELD_NAME_ID\",\"filters\":[{\"targetFilter\":{\"targetId\":\"69629023906488334\"}}]}"; + }; +} + +message ListExecutionsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Execution result = 2; +} + +message ListExecutionFunctionsRequest{} +message ListExecutionFunctionsResponse{ + // All available methods + repeated string functions = 1; +} +message ListExecutionMethodsRequest{} +message ListExecutionMethodsResponse{ + // All available methods + repeated string methods = 1; +} + +message ListExecutionServicesRequest{} +message ListExecutionServicesResponse{ + // All available methods + repeated string services = 1; +} diff --git a/proto/zitadel/resources/action/v3alpha/execution.proto b/proto/zitadel/action/v2beta/execution.proto similarity index 84% rename from proto/zitadel/resources/action/v3alpha/execution.proto rename to proto/zitadel/action/v2beta/execution.proto index 375ab02b86..e93470e5dc 100644 --- a/proto/zitadel/resources/action/v3alpha/execution.proto +++ b/proto/zitadel/action/v2beta/execution.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.resources.action.v3alpha; +package zitadel.action.v2beta; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -10,31 +10,27 @@ import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; import "google/protobuf/timestamp.proto"; import "zitadel/object/v3alpha/object.proto"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; message Execution { - // Ordered list of targets/includes called during the execution. - repeated ExecutionTargetType targets = 1; -} - -message GetExecution { - zitadel.resources.object.v3alpha.Details details = 1; - Condition condition = 2; - Execution execution = 3; -} - -message ExecutionTargetType { - oneof type { - option (validate.required) = true; - // Unique identifier of existing target to call. - string target = 1; - // Unique identifier of existing execution to include targets of. - Condition include = 2; - } + Condition condition = 1; + // The timestamp of the execution creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the execution. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Ordered list of targets called during the execution. + repeated string targets = 4; } message Condition { diff --git a/proto/zitadel/resources/action/v3alpha/query.proto b/proto/zitadel/action/v2beta/query.proto similarity index 80% rename from proto/zitadel/resources/action/v3alpha/query.proto rename to proto/zitadel/action/v2beta/query.proto index fb51543085..fe4f72f294 100644 --- a/proto/zitadel/resources/action/v3alpha/query.proto +++ b/proto/zitadel/action/v2beta/query.proto @@ -1,15 +1,16 @@ syntax = "proto3"; -package zitadel.resources.action.v3alpha; +package zitadel.action.v2beta; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/resources/action/v3alpha/execution.proto"; +import "zitadel/action/v2beta/execution.proto"; +import "zitadel/filter/v2beta/filter.proto"; message ExecutionSearchFilter { oneof filter { @@ -18,7 +19,6 @@ message ExecutionSearchFilter { InConditionsFilter in_conditions_filter = 1; ExecutionTypeFilter execution_type_filter = 2; TargetFilter target_filter = 3; - IncludeFilter include_filter = 4; } } @@ -42,14 +42,16 @@ message TargetFilter { ]; } -message IncludeFilter { - // Defines the include to query for. - Condition include = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the include" - example: "\"request.zitadel.session.v2.SessionService\""; - } - ]; +enum TargetFieldName { + TARGET_FIELD_NAME_UNSPECIFIED = 0; + TARGET_FIELD_NAME_ID = 1; + TARGET_FIELD_NAME_CREATED_DATE = 2; + TARGET_FIELD_NAME_CHANGED_DATE = 3; + TARGET_FIELD_NAME_NAME = 4; + TARGET_FIELD_NAME_TARGET_TYPE = 5; + TARGET_FIELD_NAME_URL = 6; + TARGET_FIELD_NAME_TIMEOUT = 7; + TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 8; } message TargetSearchFilter { @@ -71,7 +73,7 @@ message TargetNameFilter { } ]; // Defines which text comparison method used for the name query. - zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ + zitadel.filter.v2beta.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "defines which text equality method is used"; @@ -97,21 +99,10 @@ enum ExecutionType { EXECUTION_TYPE_FUNCTION = 4; } -enum TargetFieldName { - TARGET_FIELD_NAME_UNSPECIFIED = 0; - TARGET_FIELD_NAME_ID = 1; - TARGET_FIELD_NAME_CREATED_DATE = 2; - TARGET_FIELD_NAME_CHANGED_DATE = 3; - TARGET_FIELD_NAME_NAME = 4; - TARGET_FIELD_NAME_TARGET_TYPE = 5; - TARGET_FIELD_NAME_URL = 6; - TARGET_FIELD_NAME_TIMEOUT = 7; - TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 8; -} enum ExecutionFieldName { EXECUTION_FIELD_NAME_UNSPECIFIED = 0; EXECUTION_FIELD_NAME_ID = 1; EXECUTION_FIELD_NAME_CREATED_DATE = 2; EXECUTION_FIELD_NAME_CHANGED_DATE = 3; -} +} \ No newline at end of file diff --git a/proto/zitadel/action/v2beta/target.proto b/proto/zitadel/action/v2beta/target.proto new file mode 100644 index 0000000000..64da7960ea --- /dev/null +++ b/proto/zitadel/action/v2beta/target.proto @@ -0,0 +1,75 @@ +syntax = "proto3"; + +package zitadel.action.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; + +message Target { + // The unique identifier of the target. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the target creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the target (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + RESTWebhook rest_webhook = 5; + RESTCall rest_call = 6; + RESTAsync rest_async = 7; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, the the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + google.protobuf.Duration timeout = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + string endpoint = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + } + ]; + string signing_key = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message RESTWebhook { + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; +} + +message RESTCall { + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; +} + +message RESTAsync {} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index e1818833e0..d9f8bee2c7 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -41,7 +41,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; tags: [ @@ -1715,7 +1715,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "org.idp.read" + permission: "iam.idp.read" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -1732,7 +1732,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "org.idp.read" + permission: "iam.idp.read" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -2092,7 +2092,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "org.idp.write" + permission: "iam.idp.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -2110,7 +2110,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "org.idp.write" + permission: "iam.idp.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -2561,7 +2561,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "policy.write" + permission: "iam.policy.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -2578,7 +2578,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "policy.write" + permission: "iam.policy.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -2595,7 +2595,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "policy.write" + permission: "iam.policy.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -2612,7 +2612,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "policy.write" + permission: "iam.policy.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -2629,7 +2629,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "policy.write" + permission: "iam.policy.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -2646,7 +2646,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "policy.write" + permission: "iam.policy.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -3777,7 +3777,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "policy.delete" + permission: "iam.policy.delete" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -3972,7 +3972,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "policy.delete" + permission: "iam.policy.delete" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -6124,6 +6124,8 @@ message AddGenericOAuthProviderRequest { } ]; zitadel.idp.v1.Options provider_options = 9; + // Enable the use of Proof Key for Code Exchange (PKCE) for the OAuth2 flow. + bool use_pkce = 10; } message AddGenericOAuthProviderResponse { @@ -6191,6 +6193,8 @@ message UpdateGenericOAuthProviderRequest { } ]; zitadel.idp.v1.Options provider_options = 10; + // Enable the use of Proof Key for Code Exchange (PKCE) for the OAuth2 flow. + bool use_pkce = 11; } message UpdateGenericOAuthProviderResponse { @@ -6234,6 +6238,8 @@ message AddGenericOIDCProviderRequest { ]; zitadel.idp.v1.Options provider_options = 6; bool is_id_token_mapping = 7; + // Enable the use of Proof Key for Code Exchange (PKCE) for the OIDC flow. + bool use_pkce = 8; } message AddGenericOIDCProviderResponse { @@ -6285,6 +6291,8 @@ message UpdateGenericOIDCProviderRequest { ]; zitadel.idp.v1.Options provider_options = 7; bool is_id_token_mapping = 8; + // Enable the use of Proof Key for Code Exchange (PKCE) for the OIDC flow. + bool use_pkce = 9; } message UpdateGenericOIDCProviderResponse { @@ -6834,6 +6842,8 @@ message AddLDAPProviderRequest { google.protobuf.Duration timeout = 10; zitadel.idp.v1.LDAPAttributes attributes = 11; zitadel.idp.v1.Options provider_options = 12; + // Root_ca is for self signing certificates for TLS connections to LDAP servers it is intended to be filled with a .pem file. + bytes root_ca = 13 [(validate.rules).bytes.max_len = 12000]; } message AddLDAPProviderResponse { @@ -6855,6 +6865,8 @@ message UpdateLDAPProviderRequest { google.protobuf.Duration timeout = 11; zitadel.idp.v1.LDAPAttributes attributes = 12; zitadel.idp.v1.Options provider_options = 13; + // Root_ca is for self signing certificates for TLS connections to LDAP servers it is intended to be filled with a .pem file. + bytes root_ca = 14 [(validate.rules).bytes.max_len = 12000]; } message UpdateLDAPProviderResponse { @@ -8778,6 +8790,7 @@ message ListIAMMembersRequest { zitadel.v1.ListQuery query = 1; //criteria the client is looking for repeated zitadel.member.v1.SearchQuery queries = 2; + zitadel.member.v1.MemberFieldColumnName sorting_column = 3; } message ListIAMMembersResponse { diff --git a/proto/zitadel/app.proto b/proto/zitadel/app.proto index 999e71cabf..08359e3762 100644 --- a/proto/zitadel/app.proto +++ b/proto/zitadel/app.proto @@ -222,6 +222,11 @@ message SAMLConfig { bytes metadata_xml = 1; string metadata_url = 2; } + LoginVersion login_version = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; } enum APIAuthMethodType { diff --git a/proto/zitadel/feature/v2/feature_service.proto b/proto/zitadel/feature/v2/feature_service.proto index a89a182632..7b330d4f73 100644 --- a/proto/zitadel/feature/v2/feature_service.proto +++ b/proto/zitadel/feature/v2/feature_service.proto @@ -25,7 +25,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; @@ -113,6 +113,12 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { // reserving the proto field number. Such removal is not considered a breaking change. // Setting a removed field will effectively result in a no-op. service FeatureService { + // Set System Features + // + // Configure and set features that apply to the complete system. Only fields present in the request are set or unset. + // + // Required permissions: + // - system.feature.write rpc SetSystemFeatures (SetSystemFeaturesRequest) returns (SetSystemFeaturesResponse) { option (google.api.http) = { put: "/v2/features/system" @@ -126,8 +132,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set system level features"; - description: "Configure and set features that apply to the complete system. Only fields present in the request are set or unset." responses: { key: "200" value: { @@ -137,6 +141,12 @@ service FeatureService { }; } + // Reset System Features + // + // Deletes ALL configured features for the system, reverting the behaviors to system defaults. + // + // Required permissions: + // - system.feature.delete rpc ResetSystemFeatures (ResetSystemFeaturesRequest) returns (ResetSystemFeaturesResponse) { option (google.api.http) = { delete: "/v2/features/system" @@ -149,8 +159,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reset system level features"; - description: "Deletes ALL configured features for the system, reverting the behaviors to system defaults." responses: { key: "200" value: { @@ -160,6 +168,12 @@ service FeatureService { }; } + // Get System Features + // + // Returns all configured features for the system. Unset fields mean the feature is the current system default. + // + // Required permissions: + // - none rpc GetSystemFeatures (GetSystemFeaturesRequest) returns (GetSystemFeaturesResponse) { option (google.api.http) = { get: "/v2/features/system" @@ -167,13 +181,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "system.feature.read" + permission: "authenticated" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get system level features"; - description: "Returns all configured features for the system. Unset fields mean the feature is the current system default." responses: { key: "200" value: { @@ -183,6 +195,12 @@ service FeatureService { }; } + // Set Instance Features + // + // Configure and set features that apply to a complete instance. Only fields present in the request are set or unset. + // + // Required permissions: + // - iam.feature.write rpc SetInstanceFeatures (SetInstanceFeaturesRequest) returns (SetInstanceFeaturesResponse) { option (google.api.http) = { put: "/v2/features/instance" @@ -196,8 +214,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set instance level features"; - description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset." responses: { key: "200" value: { @@ -207,6 +223,12 @@ service FeatureService { }; } + // Reset Instance Features + // + // Deletes ALL configured features for an instance, reverting the behaviors to system defaults. + // + // Required permissions: + // - iam.feature.delete rpc ResetInstanceFeatures (ResetInstanceFeaturesRequest) returns (ResetInstanceFeaturesResponse) { option (google.api.http) = { delete: "/v2/features/instance" @@ -219,8 +241,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reset instance level features"; - description: "Deletes ALL configured features for an instance, reverting the behaviors to system defaults." responses: { key: "200" value: { @@ -230,6 +250,12 @@ service FeatureService { }; } + // Get Instance Features + // + // Returns all configured features for an instance. Unset fields mean the feature is the current system default. + // + // Required permissions: + // - none rpc GetInstanceFeatures (GetInstanceFeaturesRequest) returns (GetInstanceFeaturesResponse) { option (google.api.http) = { get: "/v2/features/instance" @@ -237,13 +263,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "iam.feature.read" + permission: "authenticated" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get instance level features"; - description: "Returns all configured features for an instance. Unset fields mean the feature is the current system default." responses: { key: "200" value: { @@ -253,6 +277,12 @@ service FeatureService { }; } + // Set Organization Features + // + // Configure and set features that apply to a complete instance. Only fields present in the request are set or unset. + // + // Required permissions: + // - org.feature.write rpc SetOrganizationFeatures (SetOrganizationFeaturesRequest) returns (SetOrganizationFeaturesResponse) { option (google.api.http) = { put: "/v2/features/organization/{organization_id}" @@ -266,8 +296,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set organization level features"; - description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset." responses: { key: "200" value: { @@ -277,6 +305,12 @@ service FeatureService { }; } + // Reset Organization Features + // + // Deletes ALL configured features for an organization, reverting the behaviors to instance defaults. + // + // Required permissions: + // - org.feature.delete rpc ResetOrganizationFeatures (ResetOrganizationFeaturesRequest) returns (ResetOrganizationFeaturesResponse) { option (google.api.http) = { delete: "/v2/features/organization/{organization_id}" @@ -284,13 +318,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.feature.write" + permission: "org.feature.delete" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reset organization level features"; - description: "Deletes ALL configured features for an organization, reverting the behaviors to instance defaults." responses: { key: "200" value: { @@ -300,6 +332,13 @@ service FeatureService { }; } + // Get Organization Features + // + // Returns all configured features for an organization. Unset fields mean the feature is the current instance default. + // + // Required permissions: + // - org.feature.read + // - no permission required for the organization the user belongs to rpc GetOrganizationFeatures(GetOrganizationFeaturesRequest) returns (GetOrganizationFeaturesResponse) { option (google.api.http) = { get: "/v2/features/organization/{organization_id}" @@ -307,13 +346,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.feature.read" + permission: "authenticated" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get organization level features"; - description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default." responses: { key: "200" value: { @@ -323,6 +360,12 @@ service FeatureService { }; } + // Set User Features + // + // Configure and set features that apply to an user. Only fields present in the request are set or unset. + // + // Required permissions: + // - user.feature.write rpc SetUserFeatures(SetUserFeatureRequest) returns (SetUserFeaturesResponse) { option (google.api.http) = { put: "/v2/features/user/{user_id}" @@ -336,8 +379,6 @@ service FeatureService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set user level features"; - description: "Configure and set features that apply to an user. Only fields present in the request are set or unset." responses: { key: "200" value: { @@ -347,6 +388,12 @@ service FeatureService { }; } + // Reset User Features + // + // Deletes ALL configured features for a user, reverting the behaviors to organization defaults. + // + // Required permissions: + // - user.feature.delete rpc ResetUserFeatures(ResetUserFeaturesRequest) returns (ResetUserFeaturesResponse) { option (google.api.http) = { delete: "/v2/features/user/{user_id}" @@ -354,13 +401,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.feature.write" + permission: "user.feature.delete" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reset user level features"; - description: "Deletes ALL configured features for a user, reverting the behaviors to organization defaults." responses: { key: "200" value: { @@ -370,6 +415,13 @@ service FeatureService { }; } + // Get User Features + // + // Returns all configured features for a user. Unset fields mean the feature is the current organization default. + // + // Required permissions: + // - user.feature.read + // - no permission required for the own user rpc GetUserFeatures(GetUserFeaturesRequest) returns (GetUserFeaturesResponse) { option (google.api.http) = { get: "/v2/features/user/{user_id}" @@ -377,13 +429,11 @@ service FeatureService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.feature.read" + permission: "authenticated" } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get organization level features"; - description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default." responses: { key: "200" value: { diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto index 3d2280fc0c..fe8d3f7a39 100644 --- a/proto/zitadel/feature/v2/instance.proto +++ b/proto/zitadel/feature/v2/instance.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetInstanceFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -43,12 +45,6 @@ message SetInstanceFeaturesRequest{ description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, @@ -106,6 +102,13 @@ message SetInstanceFeaturesRequest{ description: "Enable a newer, more performant, permission check used for v2 and v3 resource based APIs."; } ]; + + optional bool console_use_v2_user_api = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If this is enabled the console web client will use the new User v2 API for certain calls"; + } + ]; } message SetInstanceFeaturesResponse { @@ -128,6 +131,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -164,13 +169,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; @@ -225,4 +223,11 @@ message GetInstanceFeaturesResponse { description: "Enable a newer, more performant, permission check used for v2 and v3 resource based APIs."; } ]; + + FeatureFlag console_use_v2_user_api = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If this is enabled the console web client will use the new User v2 API for certain calls"; + } + ]; } diff --git a/proto/zitadel/feature/v2/system.proto b/proto/zitadel/feature/v2/system.proto index c734905fb2..d222e2a90c 100644 --- a/proto/zitadel/feature/v2/system.proto +++ b/proto/zitadel/feature/v2/system.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetSystemFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -46,13 +48,6 @@ message SetSystemFeaturesRequest{ } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]}, @@ -110,6 +105,8 @@ message ResetSystemFeaturesResponse { message GetSystemFeaturesRequest {} message GetSystemFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -146,13 +143,6 @@ message GetSystemFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/feature/v2beta/feature_service.proto b/proto/zitadel/feature/v2beta/feature_service.proto index 3c610e1c13..daa7124e8f 100644 --- a/proto/zitadel/feature/v2beta/feature_service.proto +++ b/proto/zitadel/feature/v2beta/feature_service.proto @@ -25,7 +25,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto index 865a1d2308..7717dd7556 100644 --- a/proto/zitadel/feature/v2beta/instance.proto +++ b/proto/zitadel/feature/v2beta/instance.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetInstanceFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -43,12 +45,6 @@ message SetInstanceFeaturesRequest{ description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, @@ -101,6 +97,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -137,13 +135,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/feature/v2beta/system.proto b/proto/zitadel/feature/v2beta/system.proto index 98b37ad893..624e68ec79 100644 --- a/proto/zitadel/feature/v2beta/system.proto +++ b/proto/zitadel/feature/v2beta/system.proto @@ -11,6 +11,8 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetSystemFeaturesRequest{ + reserved 6; + reserved "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -46,13 +48,6 @@ message SetSystemFeaturesRequest{ } ]; - optional bool actions = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - repeated ImprovedPerformance improved_performance = 7 [ (validate.rules).repeated.unique = true, (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]}, @@ -83,6 +78,8 @@ message ResetSystemFeaturesResponse { message GetSystemFeaturesRequest {} message GetSystemFeaturesResponse { + reserved 7; + reserved "actions"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -119,13 +116,6 @@ message GetSystemFeaturesResponse { } ]; - FeatureFlag actions = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; - } - ]; - ImprovedPerformanceFeatureFlag improved_performance = 8 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[1]"; diff --git a/proto/zitadel/filter/v2beta/filter.proto b/proto/zitadel/filter/v2beta/filter.proto new file mode 100644 index 0000000000..6aae583cde --- /dev/null +++ b/proto/zitadel/filter/v2beta/filter.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package zitadel.filter.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta;filter"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +enum TextFilterMethod { + TEXT_FILTER_METHOD_EQUALS = 0; + TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE = 1; + TEXT_FILTER_METHOD_STARTS_WITH = 2; + TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE = 3; + TEXT_FILTER_METHOD_CONTAINS = 4; + TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE = 5; + TEXT_FILTER_METHOD_ENDS_WITH = 6; + TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE = 7; +} + +enum ListFilterMethod { + LIST_FILTER_METHOD_IN = 0; +} + +enum TimestampFilterMethod { + TIMESTAMP_FILTER_METHOD_EQUALS = 0; + TIMESTAMP_FILTER_METHOD_GREATER = 1; + TIMESTAMP_FILTER_METHOD_GREATER_OR_EQUALS = 2; + TIMESTAMP_FILTER_METHOD_LESS = 3; + TIMESTAMP_FILTER_METHOD_LESS_OR_EQUALS = 4; +} + +message PaginationRequest { + // Starting point for retrieval, in combination of offset used to query a set list of objects. + uint64 offset = 1; + // limit is the maximum amount of objects returned. The default is set to 100 + // with a maximum of 1000 in the runtime configuration. + // If the limit exceeds the maximum configured ZITADEL will throw an error. + // If no limit is present the default is taken. + uint32 limit = 2; + // Asc is the sorting order. If true the list is sorted ascending, if false + // the list is sorted descending. The default is descending. + bool asc = 3; +} + +message PaginationResponse { + // Absolute number of objects matching the query, regardless of applied limit. + uint64 total_result = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"100\""; + } + ]; + // Applied limit from query, defines maximum amount of objects per request, to compare if all objects are returned. + uint64 applied_limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"100\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index cbb2be9db0..82e32aa873 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -338,6 +338,8 @@ message OAuthConfig { description: "defines how the attribute is called where ZITADEL can get the id of the user"; } ]; + // Defines if the Proof Key for Code Exchange (PKCE) is used for the authorization code flow. + bool use_pkce = 7; } message GenericOIDCConfig { @@ -365,6 +367,12 @@ message GenericOIDCConfig { description: "if true, provider information get mapped from the id token, not from the userinfo endpoint"; } ]; + // Defines if the Proof Key for Code Exchange (PKCE) is used for the authorization code flow. + bool use_pkce = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + } + ]; } message GitHubConfig { @@ -456,6 +464,7 @@ message LDAPConfig { repeated string user_filters = 7; google.protobuf.Duration timeout = 8; LDAPAttributes attributes = 9; + bytes root_ca = 10; } message SAMLConfig { diff --git a/proto/zitadel/idp/v2/idp.proto b/proto/zitadel/idp/v2/idp.proto index 784e717d3a..0c95b742f1 100644 --- a/proto/zitadel/idp/v2/idp.proto +++ b/proto/zitadel/idp/v2/idp.proto @@ -10,24 +10,23 @@ import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "google/protobuf/duration.proto"; - option go_package = "github.com/zitadel/zitadel/pkg/grpc/idp/v2;idp"; message IDP { // Unique identifier for the identity provider. - string id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\""; - } - ]; + string id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"69629023906488334\""; + } ]; zitadel.object.v2.Details details = 2; // Current state of the identity provider. IDPState state = 3; - string name = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"Google\""; - } - ]; + string name = 4 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"Google\""; + } ]; // Type of the identity provider, for example OIDC, JWT, LDAP and SAML. IDPType type = 5; // Configuration for the type of the identity provider. @@ -93,177 +92,189 @@ message IDPConfig { message JWTConfig { // The endpoint where the JWT can be extracted. string jwt_endpoint = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://accounts.google.com\""; + (validate.rules).string = {min_len : 1, max_len : 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = + { + example: + "\"https://accounts.google.com\""; } ]; // The issuer of the JWT (for validation). string issuer = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://accounts.google.com\""; + (validate.rules).string = {min_len : 1, max_len : 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = + { + example: + "\"https://accounts.google.com\""; } ]; // The endpoint to the key (JWK) which is used to sign the JWT with. string keys_endpoint = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://accounts.google.com/keys\""; + (validate.rules).string = {min_len : 1, max_len : 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = + { + example: + "\"https://accounts.google.com/keys\""; } ]; // The name of the header where the JWT is sent in, default is authorization. string header_name = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"x-auth-token\""; + (validate.rules).string = {min_len : 1, max_len : 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = + { + example: + "\"x-auth-token\""; } ]; } message OAuthConfig { // Client id generated by the identity provider. - string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - } - ]; + string client_id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + } ]; // The endpoint where ZITADEL send the user to authenticate. - string authorization_endpoint = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://accounts.google.com/o/oauth2/v2/auth\""; - } - ]; + string authorization_endpoint = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"https://accounts.google.com/o/oauth2/v2/auth\""; + } ]; // The endpoint where ZITADEL can get the token. - string token_endpoint = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://oauth2.googleapis.com/token\""; - } - ]; + string token_endpoint = 3 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"https://oauth2.googleapis.com/token\""; + } ]; // The endpoint where ZITADEL can get the user information. - string user_endpoint = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://openidconnect.googleapis.com/v1/userinfo\""; - } - ]; - // The scopes requested by ZITADEL during the request on the identity provider. - repeated string scopes = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - } - ]; - // Defines how the attribute is called where ZITADEL can get the id of the user. - string id_attribute = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"user_id\""; - } - ]; + string user_endpoint = 4 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"https://openidconnect.googleapis.com/v1/userinfo\""; + } ]; + // The scopes requested by ZITADEL during the request on the identity + // provider. + repeated string scopes = 5 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\"]"; + } ]; + // Defines how the attribute is called where ZITADEL can get the id of the + // user. + string id_attribute = 6 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"user_id\""; + } ]; } message GenericOIDCConfig { // The OIDC issuer of the identity provider. - string issuer = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://accounts.google.com/\""; - } - ]; + string issuer = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"https://accounts.google.com/\""; + } ]; // Client id generated by the identity provider. - string client_id = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - } - ]; - // The scopes requested by ZITADEL during the request on the identity provider. - repeated string scopes = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - } - ]; - // If true, provider information get mapped from the id token, not from the userinfo endpoint. - bool is_id_token_mapping = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - } - ]; + string client_id = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + } ]; + // The scopes requested by ZITADEL during the request on the identity + // provider. + repeated string scopes = 3 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\"]"; + } ]; + // If true, provider information get mapped from the id token, not from the + // userinfo endpoint. + bool is_id_token_mapping = 4 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "true"; + } ]; } message GitHubConfig { // The client ID of the GitHub App. - string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - } - ]; + string client_id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + } ]; // The scopes requested by ZITADEL during the request to GitHub. - repeated string scopes = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - } - ]; + repeated string scopes = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\"]"; + } ]; } message GitHubEnterpriseServerConfig { // The client ID of the GitHub App. - string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - } - ]; + string client_id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + } ]; string authorization_endpoint = 2; string token_endpoint = 3; string user_endpoint = 4; // The scopes requested by ZITADEL during the request to GitHub. - repeated string scopes = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - } - ]; + repeated string scopes = 5 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\"]"; + } ]; } message GoogleConfig { // Client id of the Google application. - string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - } - ]; + string client_id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + } ]; // The scopes requested by ZITADEL during the request to Google. - repeated string scopes = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - } - ]; + repeated string scopes = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\"]"; + } ]; } message GitLabConfig { // Client id of the GitLab application. - string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - } - ]; + string client_id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + } ]; // The scopes requested by ZITADEL during the request to GitLab. - repeated string scopes = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - } - ]; + repeated string scopes = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\"]"; + } ]; } message GitLabSelfHostedConfig { string issuer = 1; // Client id of the GitLab application. - string client_id = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - } - ]; + string client_id = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + } ]; // The scopes requested by ZITADEL during the request to GitLab. - repeated string scopes = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\"]"; - } - ]; + repeated string scopes = 3 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\"]"; + } ]; } message LDAPConfig { @@ -276,6 +287,7 @@ message LDAPConfig { repeated string user_filters = 7; google.protobuf.Duration timeout = 8; LDAPAttributes attributes = 9; + bytes root_ca = 10; } message SAMLConfig { @@ -288,66 +300,84 @@ message SAMLConfig { // `nameid-format` for the SAML Request. SAMLNameIDFormat name_id_format = 4; // Optional name of the attribute, which will be used to map the user - // in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. + // in case the nameid-format returned is + // `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. optional string transient_mapping_attribute_name = 5; } message AzureADConfig { // Client id of the Azure AD application - string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"client-id\""; - } - ]; - // Defines what user accounts should be able to login (Personal, Organizational, All). + string client_id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"client-id\""; + } ]; + // Defines what user accounts should be able to login (Personal, + // Organizational, All). AzureADTenant tenant = 2; - // Azure AD doesn't send if the email has been verified. Enable this if the user email should always be added verified in ZITADEL (no verification emails will be sent). + // Azure AD doesn't send if the email has been verified. Enable this if the + // user email should always be added verified in ZITADEL (no verification + // emails will be sent). bool email_verified = 3; // The scopes requested by ZITADEL during the request to Azure AD. - repeated string scopes = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"openid\", \"profile\", \"email\", \"User.Read\"]"; - } - ]; + repeated string scopes = 4 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"openid\", \"profile\", \"email\", \"User.Read\"]"; + } ]; } message Options { - // Enable if users should be able to link an existing ZITADEL user with an external account. + // Enable if users should be able to link an existing ZITADEL user with an + // external account. bool is_linking_allowed = 1; - // Enable if users should be able to create a new account in ZITADEL when using an external account. + // Enable if users should be able to create a new account in ZITADEL when + // using an external account. bool is_creation_allowed = 2; - // Enable if a new account in ZITADEL should be created automatically when login with an external account. + // Enable if a new account in ZITADEL should be created automatically when + // login with an external account. bool is_auto_creation = 3; - // Enable if a the ZITADEL account fields should be updated automatically on each login. + // Enable if a the ZITADEL account fields should be updated automatically on + // each login. bool is_auto_update = 4; - // Enable if users should get prompted to link an existing ZITADEL user to an external account if the selected attribute matches. - AutoLinkingOption auto_linking = 5 ; + // Enable if users should get prompted to link an existing ZITADEL user to an + // external account if the selected attribute matches. + AutoLinkingOption auto_linking = 5; } enum AutoLinkingOption { // AUTO_LINKING_OPTION_UNSPECIFIED disables the auto linking prompt. AUTO_LINKING_OPTION_UNSPECIFIED = 0; - // AUTO_LINKING_OPTION_USERNAME will use the username of the external user to check for a corresponding ZITADEL user. + // AUTO_LINKING_OPTION_USERNAME will use the username of the external user to + // check for a corresponding ZITADEL user. AUTO_LINKING_OPTION_USERNAME = 1; - // AUTO_LINKING_OPTION_EMAIL will use the email of the external user to check for a corresponding ZITADEL user with the same verified email - // Note that in case multiple users match, no prompt will be shown. + // AUTO_LINKING_OPTION_EMAIL will use the email of the external user to check + // for a corresponding ZITADEL user with the same verified email Note that in + // case multiple users match, no prompt will be shown. AUTO_LINKING_OPTION_EMAIL = 2; } message LDAPAttributes { - string id_attribute = 1 [(validate.rules).string = {max_len: 200}]; - string first_name_attribute = 2 [(validate.rules).string = {max_len: 200}]; - string last_name_attribute = 3 [(validate.rules).string = {max_len: 200}]; - string display_name_attribute = 4 [(validate.rules).string = {max_len: 200}]; - string nick_name_attribute = 5 [(validate.rules).string = {max_len: 200}]; - string preferred_username_attribute = 6 [(validate.rules).string = {max_len: 200}]; - string email_attribute = 7 [(validate.rules).string = {max_len: 200}]; - string email_verified_attribute = 8 [(validate.rules).string = {max_len: 200}]; - string phone_attribute = 9 [(validate.rules).string = {max_len: 200}]; - string phone_verified_attribute = 10 [(validate.rules).string = {max_len: 200}]; - string preferred_language_attribute = 11 [(validate.rules).string = {max_len: 200}]; - string avatar_url_attribute = 12 [(validate.rules).string = {max_len: 200}]; - string profile_attribute = 13 [(validate.rules).string = {max_len: 200}]; + string id_attribute = 1 [ (validate.rules).string = {max_len : 200} ]; + string first_name_attribute = 2 [ (validate.rules).string = {max_len : 200} ]; + string last_name_attribute = 3 [ (validate.rules).string = {max_len : 200} ]; + string display_name_attribute = 4 + [ (validate.rules).string = {max_len : 200} ]; + string nick_name_attribute = 5 [ (validate.rules).string = {max_len : 200} ]; + string preferred_username_attribute = 6 + [ (validate.rules).string = {max_len : 200} ]; + string email_attribute = 7 [ (validate.rules).string = {max_len : 200} ]; + string email_verified_attribute = 8 + [ (validate.rules).string = {max_len : 200} ]; + string phone_attribute = 9 [ (validate.rules).string = {max_len : 200} ]; + string phone_verified_attribute = 10 + [ (validate.rules).string = {max_len : 200} ]; + string preferred_language_attribute = 11 + [ (validate.rules).string = {max_len : 200} ]; + string avatar_url_attribute = 12 + [ (validate.rules).string = {max_len : 200} ]; + string profile_attribute = 13 [ (validate.rules).string = {max_len : 200} ]; + string root_ca= 14; } enum AzureADTenantType { @@ -365,27 +395,27 @@ message AzureADTenant { message AppleConfig { // Client id (App ID or Service ID) provided by Apple. - string client_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"com.client.id\""; - } - ]; + string client_id = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"com.client.id\""; + } ]; // Team ID provided by Apple. - string team_id = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ALT03JV3OS\""; - } - ]; + string team_id = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"ALT03JV3OS\""; + } ]; // ID of the private key generated by Apple. - string key_id = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"OGKDK25KD\""; - } - ]; + string key_id = 3 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "\"OGKDK25KD\""; + } ]; // The scopes requested by ZITADEL during the request to Apple. - repeated string scopes = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"name\", \"email\"]"; - } - ]; -} \ No newline at end of file + repeated string scopes = 4 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: + "[\"name\", \"email\"]"; + } ]; +} diff --git a/proto/zitadel/idp/v2/idp_service.proto b/proto/zitadel/idp/v2/idp_service.proto index 2d5306cea6..418704779c 100644 --- a/proto/zitadel/idp/v2/idp_service.proto +++ b/proto/zitadel/idp/v2/idp_service.proto @@ -24,7 +24,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 94de141a65..e69e331f87 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -9850,6 +9850,11 @@ message AddSAMLAppRequest { bytes metadata_xml = 3 [(validate.rules).bytes.max_len = 500000]; string metadata_url = 4 [(validate.rules).string.max_len = 200]; } + zitadel.app.v1.LoginVersion login_version = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; } message AddSAMLAppResponse { @@ -10014,6 +10019,11 @@ message UpdateSAMLAppConfigRequest { bytes metadata_xml = 3 [(validate.rules).bytes.max_len = 500000]; string metadata_url = 4 [(validate.rules).string.max_len = 200]; } + zitadel.app.v1.LoginVersion login_version = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; } message UpdateSAMLAppConfigResponse { @@ -12535,6 +12545,8 @@ message AddGenericOAuthProviderRequest { } ]; zitadel.idp.v1.Options provider_options = 9; + // Enable the use of Proof Key for Code Exchange (PKCE) for the OAuth2 flow. + bool use_pkce = 10; } message AddGenericOAuthProviderResponse { @@ -12602,6 +12614,8 @@ message UpdateGenericOAuthProviderRequest { } ]; zitadel.idp.v1.Options provider_options = 10; + // Enable the use of Proof Key for Code Exchange (PKCE) for the OAuth2 flow. + bool use_pkce = 11; } message UpdateGenericOAuthProviderResponse { @@ -12645,6 +12659,8 @@ message AddGenericOIDCProviderRequest { ]; zitadel.idp.v1.Options provider_options = 6; bool is_id_token_mapping = 7; + // Enable the use of Proof Key for Code Exchange (PKCE) for the OIDC flow. + bool use_pkce = 8; } message AddGenericOIDCProviderResponse { @@ -12696,6 +12712,8 @@ message UpdateGenericOIDCProviderRequest { ]; zitadel.idp.v1.Options provider_options = 7; bool is_id_token_mapping = 8; + // Enable the use of Proof Key for Code Exchange (PKCE) for the OIDC flow. + bool use_pkce = 9; } message UpdateGenericOIDCProviderResponse { @@ -13245,6 +13263,8 @@ message AddLDAPProviderRequest { google.protobuf.Duration timeout = 10; zitadel.idp.v1.LDAPAttributes attributes = 11; zitadel.idp.v1.Options provider_options = 12; + // Root_ca is for self signing certificates for TLS connections to LDAP servers it is intended to be filled with a .pem file. + bytes root_ca = 13 [(validate.rules).bytes.max_len = 12000]; } message AddLDAPProviderResponse { @@ -13266,6 +13286,8 @@ message UpdateLDAPProviderRequest { google.protobuf.Duration timeout = 11; zitadel.idp.v1.LDAPAttributes attributes = 12; zitadel.idp.v1.Options provider_options = 13; + // Root_ca is for self signing certificates for TLS connections to LDAP servers it is intended to be filled with a .pem file. + bytes root_ca = 14 [(validate.rules).bytes.max_len = 12000]; } message UpdateLDAPProviderResponse { @@ -13653,7 +13675,7 @@ message SetTriggerActionsRequest { * - Internal Authentication: 3 * - Complement Token: 2 * - Complement SAML Response: 4 - */ + */ string flow_type = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"1\""; @@ -13664,11 +13686,11 @@ message SetTriggerActionsRequest { * - External Authentication: * - Post Authentication: TRIGGER_TYPE_POST_AUTHENTICATION or 1 * - Pre Creation: TRIGGER_TYPE_PRE_CREATION or 2 - * - Post Creation: TRIGGER_TYPE_POST_CREATION or 3 + * - Post Creation: TRIGGER_TYPE_POST_CREATION or 3 * - Internal Authentication: * - Post Authentication: TRIGGER_TYPE_POST_AUTHENTICATION or 1 * - Pre Creation: TRIGGER_TYPE_PRE_CREATION or 2 - * - Post Creation: TRIGGER_TYPE_POST_CREATION or 3 + * - Post Creation: TRIGGER_TYPE_POST_CREATION or 3 * - Complement Token: * - Pre Userinfo Creation: 4 * - Pre Access Token Creation: 5 diff --git a/proto/zitadel/member.proto b/proto/zitadel/member.proto index c3351a99d3..f4ec080433 100644 --- a/proto/zitadel/member.proto +++ b/proto/zitadel/member.proto @@ -143,3 +143,11 @@ message UserIDQuery { } ]; } + +enum MemberFieldColumnName { + MEMBER_FIELD_NAME_UNSPECIFIED = 0; + MEMBER_FIELD_NAME_USER_ID=1; + MEMBER_FIELD_NAME_CREATION_DATE = 2; + MEMBER_FIELD_NAME_CHANGE_DATE=3; + MEMBER_FIELD_NAME_USER_RESOURCE_OWNER=4; +} \ No newline at end of file diff --git a/proto/zitadel/object/v2/object.proto b/proto/zitadel/object/v2/object.proto index 5a63ece19b..339966d3b2 100644 --- a/proto/zitadel/object/v2/object.proto +++ b/proto/zitadel/object/v2/object.proto @@ -78,6 +78,8 @@ message Details { example: "\"69629023906488334\""; } ]; + //creation_date is the timestamp where the first operation on the object was made + google.protobuf.Timestamp creation_date = 4; } message ListDetails { diff --git a/proto/zitadel/object/v2beta/object.proto b/proto/zitadel/object/v2beta/object.proto index df8e77319a..fd3190ccd6 100644 --- a/proto/zitadel/object/v2beta/object.proto +++ b/proto/zitadel/object/v2beta/object.proto @@ -78,6 +78,8 @@ message Details { example: "\"69629023906488334\""; } ]; + //creation_date is the timestamp where the first operation on the object was made + google.protobuf.Timestamp creation_date = 4; } message ListDetails { diff --git a/proto/zitadel/oidc/v2/authorization.proto b/proto/zitadel/oidc/v2/authorization.proto index c0ad751624..6cdf55de64 100644 --- a/proto/zitadel/oidc/v2/authorization.proto +++ b/proto/zitadel/oidc/v2/authorization.proto @@ -114,4 +114,17 @@ enum ErrorReason { ERROR_REASON_REQUEST_NOT_SUPPORTED = 14; ERROR_REASON_REQUEST_URI_NOT_SUPPORTED = 15; ERROR_REASON_REGISTRATION_NOT_SUPPORTED = 16; +} + +message DeviceAuthorizationRequest { + // The unique identifier of the device authorization request to be used for authorizing or denying the request. + string id = 1; + // The client_id of the application that initiated the device authorization request. + string client_id = 2; + // The scopes requested by the application. + repeated string scope = 3; + // Name of the client application. + string app_name = 4; + // Name of the project the client application is part of. + string project_name = 5; } \ No newline at end of file diff --git a/proto/zitadel/oidc/v2/oidc_service.proto b/proto/zitadel/oidc/v2/oidc_service.proto index 3c36057afa..4624910c65 100644 --- a/proto/zitadel/oidc/v2/oidc_service.proto +++ b/proto/zitadel/oidc/v2/oidc_service.proto @@ -24,7 +24,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; @@ -147,6 +147,58 @@ service OIDCService { }; }; } + + // Get device authorization request + // + // Get the device authorization based on the provided "user code". + // This will return the device authorization request, which contains the device authorization id + // that is required to authorize the request once the user signed in or to deny it. + rpc GetDeviceAuthorizationRequest(GetDeviceAuthorizationRequestRequest) returns (GetDeviceAuthorizationRequestResponse) { + option (google.api.http) = { + get: "/v2/oidc/device_authorization/{user_code}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Authorize or deny device authorization + // + // Authorize or deny the device authorization request based on the provided device authorization id. + rpc AuthorizeOrDenyDeviceAuthorization(AuthorizeOrDenyDeviceAuthorizationRequest) returns (AuthorizeOrDenyDeviceAuthorizationResponse) { + option (google.api.http) = { + post: "/v2/oidc/device_authorization/{device_authorization_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + } message GetAuthRequestRequest { @@ -217,3 +269,42 @@ message CreateCallbackResponse { ]; } +message GetDeviceAuthorizationRequestRequest { + // The user_code returned by the device authorization request and provided to the user by the device. + string user_code = 1 [ + (validate.rules).string = {len: 9}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 9; + max_length: 9; + example: "\"K9LV-3DMQ\""; + } + ]; +} + +message GetDeviceAuthorizationRequestResponse { + DeviceAuthorizationRequest device_authorization_request = 1; +} + +message AuthorizeOrDenyDeviceAuthorizationRequest { + // The device authorization id returned when submitting the user code. + string device_authorization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + } + ]; + + // The decision of the user to authorize or deny the device authorization request. + oneof decision { + option (validate.required) = true; + // To authorize the device authorization request, the user's session must be provided. + Session session = 2; + // Deny the device authorization request. + Deny deny = 3; + } +} + +message Deny{} + +message AuthorizeOrDenyDeviceAuthorizationResponse {} \ No newline at end of file diff --git a/proto/zitadel/oidc/v2beta/oidc_service.proto b/proto/zitadel/oidc/v2beta/oidc_service.proto index d962c90a91..e984e4db51 100644 --- a/proto/zitadel/oidc/v2beta/oidc_service.proto +++ b/proto/zitadel/oidc/v2beta/oidc_service.proto @@ -24,7 +24,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto index 3917fc85a6..94ced55146 100644 --- a/proto/zitadel/org/v2/org_service.proto +++ b/proto/zitadel/org/v2/org_service.proto @@ -34,7 +34,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 132f0f1f30..90c29ca354 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -33,7 +33,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto deleted file mode 100644 index bc3739861d..0000000000 --- a/proto/zitadel/resources/action/v3alpha/action_service.proto +++ /dev/null @@ -1,565 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.action.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -import "zitadel/resources/action/v3alpha/target.proto"; -import "zitadel/resources/action/v3alpha/execution.proto"; -import "zitadel/resources/action/v3alpha/query.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Action Service"; - version: "3.0-alpha"; - description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. It will continue breaking as long as it is in alpha state."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - schemes: HTTPS; - schemes: HTTP; - - consumes: "application/json"; - consumes: "application/grpc"; - - produces: "application/json"; - produces: "application/grpc"; - - consumes: "application/grpc-web+proto"; - produces: "application/grpc-web+proto"; - - host: "$CUSTOM-DOMAIN"; - base_path: "/"; - - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://zitadel.com/docs" - } - security_definitions: { - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } - security: { - security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the resource."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } -}; - -service ZITADELActions { - - // Create a target - // - // Create a new target, which can be used in executions. - rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/actions/targets" - body: "target" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "201"; - value: { - description: "Target successfully created"; - schema: { - json_schema: { - ref: "#/definitions/v3alphaCreateTargetResponse"; - } - } - }; - }; - }; - } - - // Patch a target - // - // Patch an existing target. - rpc PatchTarget (PatchTargetRequest) returns (PatchTargetResponse) { - option (google.api.http) = { - patch: "/resources/v3alpha/actions/targets/{id}" - body: "target" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.write" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully updated or left unchanged"; - }; - }; - }; - } - - // Delete a target - // - // Delete an existing target. This will remove it from any configured execution as well. - rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { - option (google.api.http) = { - delete: "/resources/v3alpha/actions/targets/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully deleted"; - }; - }; - }; - } - - // Target by ID - // - // Returns the target identified by the requested ID. - rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/actions/targets/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200" - value: { - description: "Target successfully retrieved"; - } - }; - }; - } - - // Search targets - // - // Search all matching targets. By default all targets of the instance are returned. - // Make sure to include a limit and sorting for pagination. - rpc SearchTargets (SearchTargetsRequest) returns (SearchTargetsResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/actions/targets/_search", - body: "filters" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all targets matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // Sets an execution to call a target or include the targets of another execution. - // - // Setting an empty list of targets will remove all targets from the execution, making it a noop. - rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { - option (google.api.http) = { - put: "/resources/v3alpha/actions/executions" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.execution.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Execution successfully updated or left unchanged"; - schema: { - json_schema: { - ref: "#/definitions/v3alphaSetExecutionResponse"; - } - } - }; - }; - }; - } - - // Search executions - // - // Search all matching executions. By default all executions of the instance are returned that have at least one execution target. - // Make sure to include a limit and sorting for pagination. - rpc SearchExecutions (SearchExecutionsRequest) returns (SearchExecutionsResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/actions/executions/_search" - body: "filters" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "action.execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all non noop executions matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // List all available functions - // - // List all available functions which can be used as condition for executions. - rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/actions/executions/functions" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "authenticated" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all functions successfully"; - }; - }; - }; - } - // List all available methods - // - // List all available methods which can be used as condition for executions. - rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/actions/executions/methods" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "authenticated" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all methods successfully"; - }; - }; - }; - } - // List all available service - // - // List all available services which can be used as condition for executions. - rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/actions/executions/services" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "authenticated" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all services successfully"; - }; - }; - }; - } -} - -message CreateTargetRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - Target target = 2 [ - (validate.rules).message = { - required: true - } - ]; -} - -message CreateTargetResponse { - zitadel.resources.object.v3alpha.Details details = 1; - // Key used to sign and check payload sent to the target. - string signing_key = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"98KmsU67\"" - } - ]; -} - -message PatchTargetRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - PatchTarget target = 3 [ - (validate.rules).message = { - required: true - } - ]; -} - -message PatchTargetResponse { - zitadel.resources.object.v3alpha.Details details = 1; - // Key used to sign and check payload sent to the target. - optional string signing_key = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"98KmsU67\"" - } - ]; -} - -message DeleteTargetRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message DeleteTargetResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message GetTargetRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message GetTargetResponse { - GetTarget target = 1; -} - -message SearchTargetsRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - // list limitations and ordering. - optional zitadel.resources.object.v3alpha.SearchQuery query = 2; - // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. - optional TargetFieldName sorting_column = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" - } - ]; - // Define the criteria to query for. - repeated TargetSearchFilter filters = 4; -} - -message SearchTargetsResponse { - zitadel.resources.object.v3alpha.ListDetails details = 1; - repeated GetTarget result = 2; -} - -message SetExecutionRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - Condition condition = 2; - Execution execution = 3; -} - -message SetExecutionResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message SearchExecutionsRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - // list limitations and ordering. - optional zitadel.resources.object.v3alpha.SearchQuery query = 2; - // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. - optional ExecutionFieldName sorting_column = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"EXECUTION_FIELD_NAME_CREATION_DATE\"" - } - ]; - // Define the criteria to query for. - repeated ExecutionSearchFilter filters = 4; -} - -message SearchExecutionsResponse { - zitadel.resources.object.v3alpha.ListDetails details = 1; - repeated GetExecution result = 2; -} - -message ListExecutionFunctionsRequest{} -message ListExecutionFunctionsResponse{ - // All available methods - repeated string functions = 1; -} -message ListExecutionMethodsRequest{} -message ListExecutionMethodsResponse{ - // All available methods - repeated string methods = 1; -} - -message ListExecutionServicesRequest{} -message ListExecutionServicesResponse{ - // All available methods - repeated string services = 1; -} diff --git a/proto/zitadel/resources/action/v3alpha/target.proto b/proto/zitadel/resources/action/v3alpha/target.proto deleted file mode 100644 index 8524ab3639..0000000000 --- a/proto/zitadel/resources/action/v3alpha/target.proto +++ /dev/null @@ -1,124 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.action.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; -import "google/protobuf/timestamp.proto"; - -import "zitadel/resources/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; - -message Target { - string name = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\""; - min_length: 1 - max_length: 1000 - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - option (validate.required) = true; - SetRESTWebhook rest_webhook = 2; - SetRESTCall rest_call = 3; - SetRESTAsync rest_async = 4; - } - // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 5 [ - (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "if the target doesn't respond before this timeout expires, the the connection is closed and the action fails"; - example: "\"10s\""; - } - ]; - string endpoint = 6 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://example.com/hooks/ip_check\"" - min_length: 1 - max_length: 1000 - } - ]; -} - -message GetTarget { - zitadel.resources.object.v3alpha.Details details = 1; - Target config = 2; - string signing_key = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"98KmsU67\"" - } - ]; -} - -message PatchTarget { - optional string name = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\"" - min_length: 1 - max_length: 1000 - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - SetRESTWebhook rest_webhook = 2; - SetRESTCall rest_call = 3; - SetRESTAsync rest_async = 4; - } - // Timeout defines the duration until ZITADEL cancels the execution. - optional google.protobuf.Duration timeout = 5 [ - (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "if the target doesn't respond before this timeout expires, the the connection is closed and the action fails"; - example: "\"10s\""; - } - ]; - optional string endpoint = 6 [ - (validate.rules).string = {min_len: 1, max_len: 1000}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://example.com/hooks/ip_check\"" - min_length: 1 - max_length: 1000 - } - ]; - // Regenerate the key used for signing and checking the payload sent to the target. - // Set the graceful period for the existing key. During that time, the previous - // signing key and the new one will be used to sign the request to allow you a smooth - // transition onf your API. - // - // Note that we currently only allow an immediate rotation ("0s") and will support - // longer expirations in the future. - optional google.protobuf.Duration expiration_signing_key = 7 [ - (validate.rules).duration = {const: {seconds: 0, nanos: 0}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"0s\"" - minimum: 0 - maximum: 0 - } - ]; -} - - -// Wait for response but response body is ignored, status is checked, call is sent as post. -message SetRESTWebhook { - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 1; -} - -// Wait for response and response body is used, status is checked, call is sent as post. -message SetRESTCall { - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 1; -} - -// Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. -message SetRESTAsync {} diff --git a/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto b/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto index 6a5990f783..c2b0be5226 100644 --- a/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto +++ b/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto @@ -27,7 +27,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/resources/user/v3alpha/user_service.proto b/proto/zitadel/resources/user/v3alpha/user_service.proto index 4e297d5ed1..2a7e87d923 100644 --- a/proto/zitadel/resources/user/v3alpha/user_service.proto +++ b/proto/zitadel/resources/user/v3alpha/user_service.proto @@ -30,7 +30,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto b/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto index ae9ef6ec8b..ea68923eec 100644 --- a/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto +++ b/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto @@ -27,7 +27,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/resources/webkey/v3alpha/config.proto b/proto/zitadel/resources/webkey/v3alpha/config.proto deleted file mode 100644 index 170334afa5..0000000000 --- a/proto/zitadel/resources/webkey/v3alpha/config.proto +++ /dev/null @@ -1,41 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.webkey.v3alpha; - -import "validate/validate.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; - -message WebKeyRSAConfig { - enum RSABits { - RSA_BITS_UNSPECIFIED = 0; - RSA_BITS_2048 = 1; - RSA_BITS_3072 = 2; - RSA_BITS_4096 = 3; - } - - enum RSAHasher { - RSA_HASHER_UNSPECIFIED = 0; - RSA_HASHER_SHA256 = 1; - RSA_HASHER_SHA384 = 2; - RSA_HASHER_SHA512 = 3; - } - - // bit size of the RSA key - RSABits bits = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; - // signing algrithm used - RSAHasher hasher = 2 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; -} - -message WebKeyECDSAConfig { - enum ECDSACurve { - ECDSA_CURVE_UNSPECIFIED = 0; - ECDSA_CURVE_P256 = 1; - ECDSA_CURVE_P384 = 2; - ECDSA_CURVE_P512 = 3; - } - - ECDSACurve curve = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; -} - -message WebKeyED25519Config {} diff --git a/proto/zitadel/resources/webkey/v3alpha/key.proto b/proto/zitadel/resources/webkey/v3alpha/key.proto deleted file mode 100644 index 47486f7aee..0000000000 --- a/proto/zitadel/resources/webkey/v3alpha/key.proto +++ /dev/null @@ -1,31 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.webkey.v3alpha; - -import "google/protobuf/timestamp.proto"; -import "zitadel/resources/webkey/v3alpha/config.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; - -enum WebKeyState { - STATE_UNSPECIFIED = 0; - STATE_INITIAL = 1; - STATE_ACTIVE = 2; - STATE_INACTIVE = 3; - STATE_REMOVED = 4; -} - -message GetWebKey { - zitadel.resources.object.v3alpha.Details details = 1; - WebKey config = 2; - WebKeyState state = 3; -} - -message WebKey { - oneof config { - WebKeyRSAConfig rsa = 6; - WebKeyECDSAConfig ecdsa = 7; - WebKeyED25519Config ed25519 = 8; - } -} diff --git a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto deleted file mode 100644 index 43d2edab2b..0000000000 --- a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto +++ /dev/null @@ -1,278 +0,0 @@ -syntax = "proto3"; - -package zitadel.resources.webkey.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; - -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -import "zitadel/resources/webkey/v3alpha/key.proto"; -import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/object/v3alpha/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Web key Service"; - version: "3.0-preview"; - description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in preview state. It can AND will continue breaking until a stable version is released."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - schemes: HTTPS; - schemes: HTTP; - - consumes: "application/json"; - produces: "application/json"; - - consumes: "application/grpc"; - produces: "application/grpc"; - - consumes: "application/grpc-web+proto"; - produces: "application/grpc-web+proto"; - - host: "$CUSTOM-DOMAIN"; - base_path: "/"; - - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://zitadel.com/docs" - } - security_definitions: { - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } - security: { - security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the resource."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } -}; - -service ZITADELWebKeys { - rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/web_keys" - body: "key" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "iam.web_key.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Generate a web key pair for the instance"; - description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens." - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - } - - rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) { - option (google.api.http) = { - post: "/resources/v3alpha/web_keys/{id}/_activate" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "iam.web_key.write" - } - http_response: { - success_code: 200 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Activate a signing key for the instance"; - description: "Switch the active signing web key. The previously active key will be deactivated. Note that the JWKs OIDC endpoint returns a cacheable response. Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min), as the public key may not have been propagated to caches and clients yet." - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - } - - rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) { - option (google.api.http) = { - delete: "/resources/v3alpha/web_keys/{id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "iam.web_key.delete" - } - http_response: { - success_code: 200 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete a web key pair for the instance"; - description: "Delete a web key pair. Only inactive keys can be deleted. Once a key is deleted, any tokens signed by this key will be invalid." - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - } - - rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) { - option (google.api.http) = { - get: "/resources/v3alpha/web_keys" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "iam.web_key.read" - } - http_response: { - success_code: 200 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "List web key details for the instance"; - description: "List web key details for the instance" - responses: { - key: "200" - value: { - description: "OK"; - } - }; - }; - } -} - -message CreateWebKeyRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - WebKey key = 2; -} - -message CreateWebKeyResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message ActivateWebKeyRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message ActivateWebKeyResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message DeleteWebKeyRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; - string id = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message DeleteWebKeyResponse { - zitadel.resources.object.v3alpha.Details details = 1; -} - -message ListWebKeysRequest { - optional zitadel.object.v3alpha.Instance instance = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - default: "\"domain from HOST or :authority header\"" - } - ]; -} - -message ListWebKeysResponse { - repeated GetWebKey web_keys = 1; -} \ No newline at end of file diff --git a/proto/zitadel/saml/v2/saml_service.proto b/proto/zitadel/saml/v2/saml_service.proto index 3198cf3086..c6c39886ad 100644 --- a/proto/zitadel/saml/v2/saml_service.proto +++ b/proto/zitadel/saml/v2/saml_service.proto @@ -24,7 +24,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/session/v2/session_service.proto b/proto/zitadel/session/v2/session_service.proto index 74e20263fe..6b3ef9f2a7 100644 --- a/proto/zitadel/session/v2/session_service.proto +++ b/proto/zitadel/session/v2/session_service.proto @@ -28,7 +28,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/session/v2beta/session_service.proto b/proto/zitadel/session/v2beta/session_service.proto index 6a2e731a94..554d636203 100644 --- a/proto/zitadel/session/v2beta/session_service.proto +++ b/proto/zitadel/session/v2beta/session_service.proto @@ -28,7 +28,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/settings/v2/settings_service.proto b/proto/zitadel/settings/v2/settings_service.proto index 77c20eb1c6..7f71e08da4 100644 --- a/proto/zitadel/settings/v2/settings_service.proto +++ b/proto/zitadel/settings/v2/settings_service.proto @@ -30,7 +30,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/settings/v2beta/settings_service.proto b/proto/zitadel/settings/v2beta/settings_service.proto index 88331ddc54..9404e002a7 100644 --- a/proto/zitadel/settings/v2beta/settings_service.proto +++ b/proto/zitadel/settings/v2beta/settings_service.proto @@ -30,7 +30,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 8e199aa505..f124c37a79 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -30,7 +30,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; tags: [ @@ -689,6 +689,7 @@ message ListIAMMembersRequest { zitadel.v1.ListQuery query = 1; string instance_id = 2; repeated zitadel.member.v1.SearchQuery queries = 3; + zitadel.member.v1.MemberFieldColumnName sorting_column = 4; } message ListIAMMembersResponse { diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 5457efd64e..00cb352f70 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -32,7 +32,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; @@ -2094,6 +2094,7 @@ message RetrieveIdentityProviderIntentResponse{ example: "\"163840776835432345\""; } ]; + AddHumanUserRequest add_human_user = 4; } message AddIDPLinkRequest{ diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 9ad0a7e6eb..03bc36220e 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -32,7 +32,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } license: { name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; }; }; schemes: HTTPS; diff --git a/proto/zitadel/webkey/v2beta/key.proto b/proto/zitadel/webkey/v2beta/key.proto new file mode 100644 index 0000000000..b2a8a380b2 --- /dev/null +++ b/proto/zitadel/webkey/v2beta/key.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; + +package zitadel.webkey.v2beta; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta;webkey"; + +enum State { + STATE_UNSPECIFIED = 0; + // A newly created key is in the initial state and published to the public key endpoint. + STATE_INITIAL = 1; + // The active key is used to sign tokens. Only one key can be active at a time. + STATE_ACTIVE = 2; + // The inactive key is not used to sign tokens anymore, but still published to the public key endpoint. + STATE_INACTIVE = 3; + // The removed key is not used to sign tokens anymore and not published to the public key endpoint. + STATE_REMOVED = 4; +} + +message WebKey { + // The unique identifier of the key. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the key (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // State of the key + State state = 4; + // Configured type of the key (either RSA, ECDSA or ED25519) + oneof key { + RSA rsa = 5; + ECDSA ecdsa = 6; + ED25519 ed25519 = 7; + } +} + +message RSA { + // Bit size of the RSA key. Default is 2048 bits. + RSABits bits = 1 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "RSA_BITS_2048"; + } + ]; + // Signing algrithm used. Default is SHA256. + RSAHasher hasher = 2 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "RSA_HASHER_SHA256"; + } + ]; +} + +enum RSABits { + RSA_BITS_UNSPECIFIED = 0; + // 2048 bit RSA key + RSA_BITS_2048 = 1; + // 3072 bit RSA key + RSA_BITS_3072 = 2; + // 4096 bit RSA key + RSA_BITS_4096 = 3; +} + +enum RSAHasher { + RSA_HASHER_UNSPECIFIED = 0; + // SHA256 hashing algorithm resulting in the RS256 algorithm header + RSA_HASHER_SHA256 = 1; + // SHA384 hashing algorithm resulting in the RS384 algorithm header + RSA_HASHER_SHA384 = 2; + // SHA512 hashing algorithm resulting in the RS512 algorithm header + RSA_HASHER_SHA512 = 3; +} + +message ECDSA { + // Curve of the ECDSA key. Default is P-256. + ECDSACurve curve = 1 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "ECDSA_CURVE_P256"; + } + ]; +} + +enum ECDSACurve { + ECDSA_CURVE_UNSPECIFIED = 0; + // NIST P-256 curve resulting in the ES256 algorithm header + ECDSA_CURVE_P256 = 1; + // NIST P-384 curve resulting in the ES384 algorithm header + ECDSA_CURVE_P384 = 2; + // NIST P-512 curve resulting in the ES512 algorithm header + ECDSA_CURVE_P512 = 3; +} + +message ED25519 {} diff --git a/proto/zitadel/webkey/v2beta/webkey_service.proto b/proto/zitadel/webkey/v2beta/webkey_service.proto new file mode 100644 index 0000000000..ca39be5e1c --- /dev/null +++ b/proto/zitadel/webkey/v2beta/webkey_service.proto @@ -0,0 +1,359 @@ +syntax = "proto3"; + +package zitadel.webkey.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/webkey/v2beta/key.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta;webkey"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Web key Service"; + version: "2.0-beta"; + description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This service is in beta state. It can AND will continue breaking until a stable version is released.\n\nThe public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n\nPlease make sure to enable the `web_key` feature flag on your instance to use this service."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + produces: "application/json"; + + consumes: "application/grpc"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage web keys for OIDC token signing and validation. +// The service provides methods to create, activate, delete and list web keys. +// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys. +// +// Please make sure to enable the `web_key` feature flag on your instance to use this service. +service WebKeyService { + // Create Web Key + // + // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. + // The public key can be used to validate OIDC tokens. + // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // + // If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + // + // Required permission: + // - `iam.web_key.write` + // + // Required feature flag: + // - `web_key` + rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) { + option (google.api.http) = { + post: "/v2beta/web_keys" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Web key created successfully."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled."; + } + }; + }; + } + + // Activate Web Key + // + // Switch the active signing web key. The previously active key will be deactivated. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min), + // as the public key may not have been propagated to caches and clients yet. + // + // Required permission: + // - `iam.web_key.write` + // + // Required feature flag: + // - `web_key` + rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) { + option (google.api.http) = { + post: "/v2beta/web_keys/{id}/activate" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Web key activated successfully."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled."; + } + }; + responses: { + key: "404" + value: { + description: "The web key to active does not exist."; + } + }; + }; + } + + // Delete Web Key + // + // Delete a web key pair. Only inactive keys can be deleted. Once a key is deleted, + // any tokens signed by this key will be invalid. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // In case the web key is not found, the request will return a successful response as + // the desired state is already achieved. + // You can check the change date in the response to verify if the web key was deleted during the request. + // + // Required permission: + // - `iam.web_key.delete` + // + // Required feature flag: + // - `web_key` + rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) { + option (google.api.http) = { + delete: "/v2beta/web_keys/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Web key deleted successfully."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled or the web key is currently active."; + } + }; + }; + } + + // List Web Keys + // + // List all web keys and their states. + // + // Required permission: + // - `iam.web_key.read` + // + // Required feature flag: + // - `web_key` + rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) { + option (google.api.http) = { + get: "/v2beta/web_keys" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "List of all web keys."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled."; + } + }; + }; + } +} + +message CreateWebKeyRequest { + // The key type to create (RSA, ECDSA, ED25519). + // If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + oneof key { + // Create a RSA key pair and specify the bit size and hashing algorithm. + // If no bits and hasher are provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + RSA rsa = 1; + // Create a ECDSA key pair and specify the curve. + // If no curve is provided, a ECDSA key pair with P-256 curve will be created. + ECDSA ecdsa = 2; + // Create a ED25519 key pair. + ED25519 ed25519 = 3; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}}"; + }; +} + +message CreateWebKeyResponse { + // The unique identifier of the newly created key. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ActivateWebKeyRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message ActivateWebKeyResponse { + // The timestamp of the activation of the key. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteWebKeyRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteWebKeyResponse { + // The timestamp of the deletion of the key. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListWebKeysRequest {} + +message ListWebKeysResponse { + repeated WebKey web_keys = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"id\":\"69629012906488334\",\"creationDate\":\"2024-12-18T07:50:47.492Z\",\"changeDate\":\"2024-12-18T08:04:47.492Z\",\"state\":\"STATE_ACTIVE\",\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}},{\"id\":\"69629012909346200\",\"creationDate\":\"2025-01-18T12:05:47.492Z\",\"state\":\"STATE_INITIAL\",\"ecdsa\":{\"curve\":\"ECDSA_CURVE_P256\"}}]"; + } + ]; +} \ No newline at end of file